aboutsummaryrefslogtreecommitdiff
path: root/atom/packages/vim-mode/lib
diff options
context:
space:
mode:
authorBen Beltran <ben@nsovocal.com>2015-09-28 12:39:59 -0500
committerBen Beltran <ben@nsovocal.com>2015-09-28 12:39:59 -0500
commit455f099b504ec07dd492ab31987ae05a6f6774c7 (patch)
tree22b0c7fd8e492733b97e661750a2d79c46da451d /atom/packages/vim-mode/lib
parent24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a (diff)
Adds atom packages
Diffstat (limited to 'atom/packages/vim-mode/lib')
-rw-r--r--atom/packages/vim-mode/lib/global-vim-state.coffee1
-rw-r--r--atom/packages/vim-mode/lib/insert-mode.coffee19
-rw-r--r--atom/packages/vim-mode/lib/motions/find-motion.coffee50
-rw-r--r--atom/packages/vim-mode/lib/motions/general-motions.coffee115
-rw-r--r--atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee2
-rw-r--r--atom/packages/vim-mode/lib/motions/search-motion.coffee14
-rw-r--r--atom/packages/vim-mode/lib/operators/general-operators.coffee77
-rw-r--r--atom/packages/vim-mode/lib/operators/indent-operators.coffee21
-rw-r--r--atom/packages/vim-mode/lib/operators/input.coffee104
-rw-r--r--atom/packages/vim-mode/lib/operators/put-operator.coffee4
-rw-r--r--atom/packages/vim-mode/lib/operators/replace-operator.coffee6
-rw-r--r--atom/packages/vim-mode/lib/prefixes.coffee2
-rw-r--r--atom/packages/vim-mode/lib/scroll.coffee24
-rw-r--r--atom/packages/vim-mode/lib/status-bar-manager.coffee9
-rw-r--r--atom/packages/vim-mode/lib/text-objects.coffee81
-rw-r--r--atom/packages/vim-mode/lib/utils.coffee13
-rw-r--r--atom/packages/vim-mode/lib/view-models/search-view-model.coffee8
-rw-r--r--atom/packages/vim-mode/lib/view-models/view-model.coffee7
-rw-r--r--atom/packages/vim-mode/lib/view-models/vim-normal-mode-input-element.coffee (renamed from atom/packages/vim-mode/lib/view-models/vim-command-mode-input-element.coffee)38
-rw-r--r--atom/packages/vim-mode/lib/vim-mode.coffee11
-rw-r--r--atom/packages/vim-mode/lib/vim-state.coffee166
21 files changed, 509 insertions, 263 deletions
diff --git a/atom/packages/vim-mode/lib/global-vim-state.coffee b/atom/packages/vim-mode/lib/global-vim-state.coffee
index 25da977..acda507 100644
--- a/atom/packages/vim-mode/lib/global-vim-state.coffee
+++ b/atom/packages/vim-mode/lib/global-vim-state.coffee
@@ -3,3 +3,4 @@ class GlobalVimState
registers: {}
searchHistory: []
currentSearch: {}
+ currentFind: null
diff --git a/atom/packages/vim-mode/lib/insert-mode.coffee b/atom/packages/vim-mode/lib/insert-mode.coffee
new file mode 100644
index 0000000..d600e7e
--- /dev/null
+++ b/atom/packages/vim-mode/lib/insert-mode.coffee
@@ -0,0 +1,19 @@
+copyCharacterFromAbove = (editor, vimState) ->
+ editor.transact ->
+ for cursor in editor.getCursors()
+ {row, column} = cursor.getScreenPosition()
+ continue if row is 0
+ range = [[row-1, column], [row-1, column+1]]
+ cursor.selection.insertText(editor.getTextInBufferRange(editor.bufferRangeForScreenRange(range)))
+
+copyCharacterFromBelow = (editor, vimState) ->
+ editor.transact ->
+ for cursor in editor.getCursors()
+ {row, column} = cursor.getScreenPosition()
+ range = [[row+1, column], [row+1, column+1]]
+ cursor.selection.insertText(editor.getTextInBufferRange(editor.bufferRangeForScreenRange(range)))
+
+module.exports = {
+ copyCharacterFromAbove,
+ copyCharacterFromBelow
+}
diff --git a/atom/packages/vim-mode/lib/motions/find-motion.coffee b/atom/packages/vim-mode/lib/motions/find-motion.coffee
index 58685d7..2a577a8 100644
--- a/atom/packages/vim-mode/lib/motions/find-motion.coffee
+++ b/atom/packages/vim-mode/lib/motions/find-motion.coffee
@@ -3,14 +3,27 @@
{Point, Range} = require 'atom'
class Find extends MotionWithInput
- constructor: (@editor, @vimState) ->
+ operatesInclusively: true
+
+ constructor: (@editor, @vimState, opts={}) ->
super(@editor, @vimState)
- @vimState.currentFind = this
- @viewModel = new ViewModel(this, class: 'find', singleChar: true, hidden: true)
- @backwards = false
- @repeatReversed = false
@offset = 0
- @repeated = false
+
+ if not opts.repeated
+ @viewModel = new ViewModel(this, class: 'find', singleChar: true, hidden: true)
+ @backwards = false
+ @repeated = false
+ @vimState.globalVimState.currentFind = this
+
+ else
+ @repeated = true
+
+ orig = @vimState.globalVimState.currentFind
+ @backwards = orig.backwards
+ @complete = orig.complete
+ @input = orig.input
+
+ @reverse() if opts.reverse
match: (cursor, count) ->
currentPosition = cursor.getBufferPosition()
@@ -38,17 +51,22 @@ class Find extends MotionWithInput
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)
+ constructor: (@editor, @vimState, opts={}) ->
+ super(@editor, @vimState, opts)
@offset = 1
+ match: ->
+ @selectAtLeastOne = false
+ retval = super
+ if retval? and not @backwards
+ @selectAtLeastOne = true
+ retval
+
+ moveSelectionInclusively: (selection, count, options) ->
+ super
+ if selection.isEmpty() and @selectAtLeastOne
+ selection.modifySelection ->
+ selection.cursor.moveRight()
+
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
index 47a3cc7..b5cd627 100644
--- a/atom/packages/vim-mode/lib/motions/general-motions.coffee
+++ b/atom/packages/vim-mode/lib/motions/general-motions.coffee
@@ -11,7 +11,7 @@ class MotionError
@name = 'Motion Error'
class Motion
- operatesInclusively: true
+ operatesInclusively: false
operatesLinewise: false
constructor: (@editor, @vimState) ->
@@ -20,7 +20,9 @@ class Motion
value = for selection in @editor.getSelections()
if @isLinewise()
@moveSelectionLinewise(selection, count, options)
- else if @isInclusive()
+ else if @vimState.mode is 'visual'
+ @moveSelectionVisual(selection, count, options)
+ else if @operatesInclusively
@moveSelectionInclusively(selection, count, options)
else
@moveSelection(selection, count, options)
@@ -61,10 +63,27 @@ class Motion
selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]])
moveSelectionInclusively: (selection, count, options) ->
+ return @moveSelectionVisual(selection, count, options) unless selection.isEmpty()
+
+ selection.modifySelection =>
+ @moveCursor(selection.cursor, count, options)
+ return if selection.isEmpty()
+
+ if selection.isReversed()
+ # for backward motion, add the original starting character of the motion
+ {start, end} = selection.getBufferRange()
+ selection.setBufferRange([start, [end.row, end.column + 1]])
+ else
+ # for forward motion, add the ending character of the motion
+ selection.cursor.moveRight()
+
+ moveSelectionVisual: (selection, count, options) ->
selection.modifySelection =>
range = selection.getBufferRange()
[oldStart, oldEnd] = [range.start, range.end]
+ # in visual mode, atom cursor is after the last selected character,
+ # so here put cursor in the expected place for the following motion
wasEmpty = selection.isEmpty()
wasReversed = selection.isReversed()
unless wasEmpty or wasReversed
@@ -72,6 +91,7 @@ class Motion
@moveCursor(selection.cursor, count, options)
+ # put cursor back after the last character so it is also selected
isEmpty = selection.isEmpty()
isReversed = selection.isReversed()
unless isEmpty or isReversed
@@ -80,23 +100,25 @@ class Motion
range = selection.getBufferRange()
[newStart, newEnd] = [range.start, range.end]
+ # if we reversed or emptied a normal selection
+ # we need to select again the last character deselected above the motion
if (isReversed or isEmpty) and not (wasReversed or wasEmpty)
selection.setBufferRange([newStart, [newEnd.row, oldStart.column + 1]])
+
+ # if we re-reversed a reversed non-empty selection,
+ # we need to keep the last character of the old selection selected
if wasReversed and not wasEmpty and not isReversed
- selection.setBufferRange([[newStart.row, oldEnd.column - 1], newEnd])
+ selection.setBufferRange([[oldEnd.row, oldEnd.column - 1], newEnd])
+
+ # keep a single-character selection non-reversed
+ range = selection.getBufferRange()
+ [newStart, newEnd] = [range.start, range.end]
+ if selection.isReversed() and newStart.row is newEnd.row and newStart.column + 1 is newEnd.column
+ selection.setBufferRange(range, reversed: false)
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
@@ -107,21 +129,41 @@ class Motion
else
@operatesLinewise
- isInclusive: ->
- @vimState.mode is 'visual' or @operatesInclusively
-
class CurrentSelection extends Motion
constructor: (@editor, @vimState) ->
super(@editor, @vimState)
- @selection = @editor.getSelectedBufferRanges()
+ @lastSelectionRange = @editor.getSelectedBufferRange()
+ @wasLinewise = @isLinewise()
execute: (count=1) ->
_.times(count, -> true)
select: (count=1) ->
- @editor.setSelectedBufferRanges(@selection)
+ # in visual mode, the current selections are already there
+ # if we're not in visual mode, we are repeating some operation and need to re-do the selections
+ unless @vimState.mode is 'visual'
+ if @wasLinewise
+ @selectLines()
+ else
+ @selectCharacters()
+
_.times(count, -> true)
+ selectLines: ->
+ lastSelectionExtent = @lastSelectionRange.getExtent()
+ for selection in @editor.getSelections()
+ cursor = selection.cursor.getBufferPosition()
+ selection.setBufferRange [[cursor.row, 0], [cursor.row + lastSelectionExtent.row, 0]]
+ return
+
+ selectCharacters: ->
+ lastSelectionExtent = @lastSelectionRange.getExtent()
+ for selection in @editor.getSelections()
+ {start} = selection.getBufferRange()
+ newEnd = start.traverse(lastSelectionExtent)
+ selection.setBufferRange([start, newEnd])
+ return
+
# Public: Generic class for motions that require extra input
class MotionWithInput extends Motion
constructor: (@editor, @vimState) ->
@@ -139,16 +181,11 @@ class MotionWithInput extends Motion
@complete = true
class MoveLeft extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
- _.times count, =>
+ _.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()
@@ -159,16 +196,14 @@ class MoveRight extends Motion
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, =>
+ _.times count, ->
unless cursor.getScreenRow() is 0
cursor.moveUp()
- @ensureCursorIsWithinLine(cursor)
class MoveDown extends Motion
operatesLinewise: true
@@ -177,18 +212,13 @@ class MoveDown extends Motion
_.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()
@@ -205,7 +235,6 @@ class MoveToPreviousWholeWord extends Motion
class MoveToNextWord extends Motion
wordRegex: null
- operatesInclusively: false
moveCursor: (cursor, count=1, options) ->
_.times count, =>
@@ -236,6 +265,7 @@ class MoveToNextWholeWord extends MoveToNextWord
wordRegex: WholeWordOrEmptyLineRegex
class MoveToEndOfWord extends Motion
+ operatesInclusively: true
wordRegex: null
moveCursor: (cursor, count=1) ->
@@ -260,8 +290,6 @@ class MoveToEndOfWholeWord extends MoveToEndOfWord
wordRegex: WholeWordRegex
class MoveToNextParagraph extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
_.times count, ->
cursor.moveToBeginningOfNextParagraph()
@@ -284,8 +312,6 @@ class MoveToAbsoluteLine extends MoveToLine
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])
@@ -300,15 +326,11 @@ class MoveToScreenLine extends MoveToLine
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()
@@ -316,22 +338,18 @@ class MoveToFirstCharacterOfLine extends Motion
class MoveToFirstCharacterOfLineAndDown extends Motion
operatesLinewise: true
- operatesInclusively: true
- moveCursor: (cursor, count=0) ->
+ moveCursor: (cursor, count=1) ->
_.times count-1, ->
cursor.moveDown()
cursor.moveToBeginningOfLine()
cursor.moveToFirstCharacterOfLine()
class MoveToLastCharacterOfLine extends Motion
- operatesInclusively: false
-
moveCursor: (cursor, count=1) ->
- _.times count, =>
+ _.times count, ->
cursor.moveToEndOfLine()
cursor.goalColumn = Infinity
- @ensureCursorIsWithinLine(cursor)
class MoveToLastNonblankCharacterOfLineAndDown extends Motion
operatesInclusively: true
@@ -354,7 +372,6 @@ class MoveToLastNonblankCharacterOfLineAndDown extends Motion
class MoveToFirstCharacterOfLineUp extends Motion
operatesLinewise: true
- operatesInclusively: true
moveCursor: (cursor, count=1) ->
_.times count, ->
@@ -398,7 +415,7 @@ class MoveToBottomOfScreen extends MoveToScreenLine
lastScreenRow - offset
class MoveToMiddleOfScreen extends MoveToScreenLine
- getDestinationRow: (count) ->
+ getDestinationRow: ->
firstScreenRow = @editorElement.getFirstVisibleScreenRow()
lastScreenRow = @editorElement.getLastVisibleScreenRow()
height = lastScreenRow - firstScreenRow
@@ -428,7 +445,7 @@ class ScrollKeepingCursor extends MoveToLine
{row, column} = @editor.getCursorScreenPosition()
@currentFirstScreenRow - @previousFirstScreenRow + row
- scrollScreen: (count = 1) ->
+ scrollScreen: (count=1) ->
@previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
destination = @scrollDestination(count)
@editor.setScrollTop(destination)
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
index 0187620..3603e1e 100644
--- a/atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee
+++ b/atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee
@@ -4,8 +4,6 @@
module.exports =
class MoveToMark extends MotionWithInput
- operatesInclusively: false
-
constructor: (@editor, @vimState, @linewise=true) ->
super(@editor, @vimState)
@operatesLinewise = @linewise
diff --git a/atom/packages/vim-mode/lib/motions/search-motion.coffee b/atom/packages/vim-mode/lib/motions/search-motion.coffee
index 11761ad..fa65e19 100644
--- a/atom/packages/vim-mode/lib/motions/search-motion.coffee
+++ b/atom/packages/vim-mode/lib/motions/search-motion.coffee
@@ -6,8 +6,6 @@ SearchViewModel = require '../view-models/search-view-model'
settings = require '../settings'
class SearchBase extends MotionWithInput
- operatesInclusively: false
-
constructor: (@editor, @vimState, options = {}) ->
super(@editor, @vimState)
@reverse = @initiallyReversed = false
@@ -27,6 +25,8 @@ class SearchBase extends MotionWithInput
atom.beep()
scan: (cursor) ->
+ return [] if @input.characters is ""
+
currentPosition = cursor.getBufferPosition()
[rangesBefore, rangesAfter] = [[], []]
@@ -75,6 +75,16 @@ class Search extends SearchBase
constructor: (@editor, @vimState) ->
super(@editor, @vimState)
@viewModel = new SearchViewModel(this)
+ @updateViewModel()
+
+ reversed: =>
+ @initiallyReversed = @reverse = true
+ @updateCurrentSearch()
+ @updateViewModel()
+ this
+
+ updateViewModel: ->
+ @viewModel.update(@initiallyReversed)
class SearchCurrentWord extends SearchBase
@keywordRegex: null
diff --git a/atom/packages/vim-mode/lib/operators/general-operators.coffee b/atom/packages/vim-mode/lib/operators/general-operators.coffee
index dd99256..9547941 100644
--- a/atom/packages/vim-mode/lib/operators/general-operators.coffee
+++ b/atom/packages/vim-mode/lib/operators/general-operators.coffee
@@ -12,11 +12,8 @@ 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}={}) ->
+ constructor: (@editor, @vimState) ->
@complete = false
# Public: Determines when the command can be executed.
@@ -76,14 +73,9 @@ class OperatorWithInput extends Operator
#
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}={}) ->
+ constructor: (@editor, @vimState) ->
@complete = false
- @selectOptions ?= {}
- @selectOptions.requireEOL ?= true
@register = settings.defaultRegister()
# Public: Deletes the text selected by the given motion.
@@ -92,27 +84,28 @@ class Delete extends Operator
#
# Returns nothing.
execute: (count) ->
- if _.contains(@motion.select(count, @selectOptions), true)
+ if _.contains(@motion.select(count), true)
@setTextRegister(@register, @editor.getSelectedText())
- for selection in @editor.getSelections()
- selection.deleteSelectedText()
+ @editor.transact =>
+ 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()
+ @vimState.activateNormalMode()
#
# It toggles the case of everything selected by the following motion
#
class ToggleCase extends Operator
- constructor: (@editor, @vimState, {@complete, @selectOptions}={}) ->
+ constructor: (@editor, @vimState, {@complete}={}) ->
- execute: (count=1) ->
+ execute: (count) ->
if @motion?
- if _.contains(@motion.select(count, @selectOptions), true)
+ if _.contains(@motion.select(count), true)
@editor.replaceSelectedText {}, (text) ->
text.split('').map((char) ->
lower = char.toLowerCase()
@@ -126,7 +119,7 @@ class ToggleCase extends Operator
for cursor in @editor.getCursors()
point = cursor.getBufferPosition()
lineLength = @editor.lineTextForBufferRow(point.row).length
- cursorCount = Math.min(count, lineLength - point.column)
+ cursorCount = Math.min(count ? 1, lineLength - point.column)
_.times cursorCount, =>
point = cursor.getBufferPosition()
@@ -140,35 +133,35 @@ class ToggleCase extends Operator
cursor.moveRight() unless point.column >= lineLength - 1
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
#
# In visual mode or after `g` with a motion, it makes the selection uppercase
#
class UpperCase extends Operator
- constructor: (@editor, @vimState, {@selectOptions}={}) ->
+ constructor: (@editor, @vimState) ->
@complete = false
- execute: (count=1) ->
- if _.contains(@motion.select(count, @selectOptions), true)
+ execute: (count) ->
+ if _.contains(@motion.select(count), true)
@editor.replaceSelectedText {}, (text) ->
text.toUpperCase()
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
#
# In visual mode or after `g` with a motion, it makes the selection lowercase
#
class LowerCase extends Operator
- constructor: (@editor, @vimState, {@selectOptions}={}) ->
+ constructor: (@editor, @vimState) ->
@complete = false
- execute: (count=1) ->
- if _.contains(@motion.select(count, @selectOptions), true)
+ execute: (count) ->
+ if _.contains(@motion.select(count), true)
@editor.replaceSelectedText {}, (text) ->
text.toLowerCase()
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
#
# It copies everything selected by the following motion.
@@ -176,7 +169,7 @@ class LowerCase extends Operator
class Yank extends Operator
register: null
- constructor: (@editor, @vimState, {@allowEOL, @selectOptions}={}) ->
+ constructor: (@editor, @vimState) ->
@register = settings.defaultRegister()
# Public: Copies the text selected by the given motion.
@@ -185,13 +178,20 @@ class Yank extends Operator
#
# Returns nothing.
execute: (count) ->
+ oldTop = @editor.getScrollTop()
+ oldLeft = @editor.getScrollLeft()
+ oldLastCursorPosition = @editor.getCursorBufferPosition()
+
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])
+ if startPositions[i]
+ position = Point.min(startPositions[i], originalPositions[i])
+ if @vimState.mode isnt 'visual' and @motion.isLinewise?()
+ position = new Point(position.row, originalPositions[i].column)
+ position
else
originalPosition
else
@@ -201,13 +201,18 @@ class Yank extends Operator
@setTextRegister(@register, text)
@editor.setSelectedBufferRanges(newPositions.map (p) -> new Range(p, p))
- @vimState.activateCommandMode()
+
+ if oldLastCursorPosition.isEqual(@editor.getCursorBufferPosition())
+ @editor.setScrollLeft(oldLeft)
+ @editor.setScrollTop(oldTop)
+
+ @vimState.activateNormalMode()
#
# It combines the current line with the following line.
#
class Join extends Operator
- constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true
+ constructor: (@editor, @vimState) -> @complete = true
# Public: Combines the current with the following lines
#
@@ -218,13 +223,13 @@ class Join extends Operator
@editor.transact =>
_.times count, =>
@editor.joinLines()
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
#
# Repeat the last operation
#
class Repeat extends Operator
- constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true
+ constructor: (@editor, @vimState) -> @complete = true
isRecordable: -> false
@@ -237,7 +242,7 @@ class Repeat extends Operator
# It creates a mark at the current cursor position
#
class Mark extends OperatorWithInput
- constructor: (@editor, @vimState, {@selectOptions}={}) ->
+ constructor: (@editor, @vimState) ->
super(@editor, @vimState)
@viewModel = new ViewModel(this, class: 'mark', singleChar: true, hidden: true)
@@ -247,7 +252,7 @@ class Mark extends OperatorWithInput
# Returns nothing.
execute: ->
@vimState.setMark(@input.characters, @editor.getCursorBufferPosition())
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
module.exports = {
Operator, OperatorWithInput, OperatorError, Delete, ToggleCase,
diff --git a/atom/packages/vim-mode/lib/operators/indent-operators.coffee b/atom/packages/vim-mode/lib/operators/indent-operators.coffee
index 0ad37e0..ba3c155 100644
--- a/atom/packages/vim-mode/lib/operators/indent-operators.coffee
+++ b/atom/packages/vim-mode/lib/operators/indent-operators.coffee
@@ -1,17 +1,24 @@
+_ = require 'underscore-plus'
{Operator} = require './general-operators'
class AdjustIndentation extends Operator
- execute: (count=1) ->
+ execute: (count) ->
mode = @vimState.mode
@motion.select(count)
- {start} = @editor.getSelectedBufferRange()
+ originalRanges = @editor.getSelectedBufferRanges()
- @indent()
+ if mode is 'visual'
+ @editor.transact =>
+ _.times(count ? 1, => @indent())
+ else
+ @indent()
- if mode isnt 'visual'
- @editor.setCursorBufferPosition([start.row, 0])
- @editor.moveToFirstCharacterOfLine()
- @vimState.activateCommandMode()
+ @editor.clearSelections()
+ @editor.getLastCursor().setBufferPosition([originalRanges.shift().start.row, 0])
+ for range in originalRanges
+ @editor.addCursorAtBufferPosition([range.start.row, 0])
+ @editor.moveToFirstCharacterOfLine()
+ @vimState.activateNormalMode()
class Indent extends AdjustIndentation
indent: ->
diff --git a/atom/packages/vim-mode/lib/operators/input.coffee b/atom/packages/vim-mode/lib/operators/input.coffee
index 3821045..0fee121 100644
--- a/atom/packages/vim-mode/lib/operators/input.coffee
+++ b/atom/packages/vim-mode/lib/operators/input.coffee
@@ -1,3 +1,4 @@
+Motions = require '../motions/index'
{Operator, Delete} = require './general-operators'
_ = require 'underscore-plus'
settings = require '../settings'
@@ -18,7 +19,7 @@ class Insert extends Operator
execute: ->
if @typingCompleted
return unless @typedText? and @typedText.length > 0
- @editor.insertText(@typedText, normalizeLineEndings: true)
+ @editor.insertText(@typedText, normalizeLineEndings: true, autoIndent: true)
for cursor in @editor.getCursors()
cursor.moveLeft() unless cursor.isAtBeginningOfLine()
else
@@ -28,6 +29,26 @@ class Insert extends Operator
inputOperator: -> true
+class ReplaceMode extends Insert
+
+ execute: ->
+ if @typingCompleted
+ return unless @typedText? and @typedText.length > 0
+ @editor.transact =>
+ @editor.insertText(@typedText, normalizeLineEndings: true)
+ toDelete = @typedText.length - @countChars('\n', @typedText)
+ for selection in @editor.getSelections()
+ count = toDelete
+ selection.delete() while count-- and not selection.cursor.isAtEndOfLine()
+ for cursor in @editor.getCursors()
+ cursor.moveLeft() unless cursor.isAtBeginningOfLine()
+ else
+ @vimState.activateReplaceMode()
+ @typingCompleted = true
+
+ countChars: (char, string) ->
+ string.split(char).length - 1
+
class InsertAfter extends Insert
execute: ->
@editor.moveRight() unless @editor.getLastCursor().isAtEndOfLine()
@@ -45,7 +66,7 @@ class InsertAtBeginningOfLine extends Insert
super
class InsertAboveWithNewline extends Insert
- execute: (count=1) ->
+ execute: ->
@vimState.setInsertionCheckpoint() unless @typingCompleted
@editor.insertNewlineAbove()
@editor.getLastCursor().skipLeadingWhitespace()
@@ -60,7 +81,7 @@ class InsertAboveWithNewline extends Insert
@typingCompleted = true
class InsertBelowWithNewline extends Insert
- execute: (count=1) ->
+ execute: ->
@vimState.setInsertionCheckpoint() unless @typingCompleted
@editor.insertNewlineBelow()
@editor.getLastCursor().skipLeadingWhitespace()
@@ -81,7 +102,7 @@ class Change extends Insert
standalone: false
register: null
- constructor: (@editor, @vimState, {@selectOptions}={}) ->
+ constructor: (@editor, @vimState) ->
@register = settings.defaultRegister()
# Public: Changes the text selected by the given motion.
@@ -90,67 +111,29 @@ class Change extends Insert
#
# 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)
+ # If we've typed, we're being repeated. If we're being repeated,
+ # undo transactions are already handled.
+ @vimState.setInsertionCheckpoint() unless @typingCompleted
+
@setTextRegister(@register, @editor.getSelectedText())
- if @motion.isLinewise?()
- @editor.insertNewline()
- @editor.moveLeft()
+ if @motion.isLinewise?() and not @typingCompleted
+ for selection in @editor.getSelections()
+ if selection.getBufferRange().end.row is 0
+ selection.deleteSelectedText()
+ else
+ selection.insertText("\n", autoIndent: true)
+ selection.cursor.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()
+ return super if @typingCompleted
- 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
+ @vimState.activateInsertMode()
+ @typingCompleted = true
+ else
+ @vimState.activateNormalMode()
# Takes a transaction and turns it into a string of what was typed.
# This class is an implementation detail of Insert
@@ -225,7 +208,6 @@ module.exports = {
InsertAtBeginningOfLine,
InsertAboveWithNewline,
InsertBelowWithNewline,
- Change,
- Substitute,
- SubstituteLine
+ ReplaceMode,
+ Change
}
diff --git a/atom/packages/vim-mode/lib/operators/put-operator.coffee b/atom/packages/vim-mode/lib/operators/put-operator.coffee
index 63151f1..611d6fa 100644
--- a/atom/packages/vim-mode/lib/operators/put-operator.coffee
+++ b/atom/packages/vim-mode/lib/operators/put-operator.coffee
@@ -9,7 +9,7 @@ module.exports =
class Put extends Operator
register: null
- constructor: (@editor, @vimState, {@location, @selectOptions}={}) ->
+ constructor: (@editor, @vimState, {@location}={}) ->
@location ?= 'after'
@complete = true
@register = settings.defaultRegister()
@@ -58,9 +58,9 @@ class Put extends Operator
@editor.setCursorScreenPosition(originalPosition)
@editor.moveToFirstCharacterOfLine()
- @vimState.activateCommandMode()
if type isnt 'linewise'
@editor.moveLeft()
+ @vimState.activateNormalMode()
# Private: Helper to determine if the editor is currently on the last row.
#
diff --git a/atom/packages/vim-mode/lib/operators/replace-operator.coffee b/atom/packages/vim-mode/lib/operators/replace-operator.coffee
index 52157d0..9cf44b3 100644
--- a/atom/packages/vim-mode/lib/operators/replace-operator.coffee
+++ b/atom/packages/vim-mode/lib/operators/replace-operator.coffee
@@ -5,7 +5,7 @@ _ = require 'underscore-plus'
module.exports =
class Replace extends OperatorWithInput
- constructor: (@editor, @vimState, {@selectOptions}={}) ->
+ constructor: (@editor, @vimState) ->
super(@editor, @vimState)
@viewModel = new ViewModel(this, class: 'replace', hidden: true, singleChar: true, defaultText: '\n')
@@ -16,7 +16,7 @@ class Replace extends OperatorWithInput
if @vimState.mode is "visual"
@vimState.resetVisualMode()
else
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
return
@@ -47,4 +47,4 @@ class Replace extends OperatorWithInput
@editor.moveDown()
@editor.moveToFirstCharacterOfLine()
- @vimState.activateCommandMode()
+ @vimState.activateNormalMode()
diff --git a/atom/packages/vim-mode/lib/prefixes.coffee b/atom/packages/vim-mode/lib/prefixes.coffee
index b29bc92..c910963 100644
--- a/atom/packages/vim-mode/lib/prefixes.coffee
+++ b/atom/packages/vim-mode/lib/prefixes.coffee
@@ -27,7 +27,7 @@ class Prefix
@composedObject.select?(@count)
isLinewise: ->
- @composedObject.isLinewise()
+ @composedObject.isLinewise?()
#
# Used to track the number of times either a motion or operator should
diff --git a/atom/packages/vim-mode/lib/scroll.coffee b/atom/packages/vim-mode/lib/scroll.coffee
index 1bc1258..275f0f9 100644
--- a/atom/packages/vim-mode/lib/scroll.coffee
+++ b/atom/packages/vim-mode/lib/scroll.coffee
@@ -84,5 +84,27 @@ class ScrollCursorToBottom extends ScrollCursor
moveToFirstNonBlank: ->
@editor.moveToFirstCharacterOfLine()
+class ScrollHorizontal
+ isComplete: -> true
+ isRecordable: -> false
+ constructor: (@editorElement) ->
+ @editor = @editorElement.getModel()
+ cursorPos = @editor.getCursorScreenPosition()
+ @pixel = @editorElement.pixelPositionForScreenPosition(cursorPos).left
+ @cursor = @editor.getLastCursor()
+
+ putCursorOnScreen: ->
+ @editor.scrollToCursorPosition({center: false})
+
+class ScrollCursorToLeft extends ScrollHorizontal
+ execute: ->
+ @editor.setScrollLeft(@pixel)
+ @putCursorOnScreen()
+
+class ScrollCursorToRight extends ScrollHorizontal
+ execute: ->
+ @editor.setScrollRight(@pixel)
+ @putCursorOnScreen()
+
module.exports = {ScrollDown, ScrollUp, ScrollCursorToTop, ScrollCursorToMiddle,
- ScrollCursorToBottom}
+ ScrollCursorToBottom, ScrollCursorToLeft, ScrollCursorToRight}
diff --git a/atom/packages/vim-mode/lib/status-bar-manager.coffee b/atom/packages/vim-mode/lib/status-bar-manager.coffee
index 82e35e1..080e4bf 100644
--- a/atom/packages/vim-mode/lib/status-bar-manager.coffee
+++ b/atom/packages/vim-mode/lib/status-bar-manager.coffee
@@ -1,9 +1,7 @@
-{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"]
+ 'normal': ["status-bar-vim-mode-normal", "Normal"]
'visual': ["status-bar-vim-mode-visual", "Visual"]
'visual.characterwise': ["status-bar-vim-mode-visual", "Visual"]
'visual.linewise': ["status-bar-vim-mode-visual", "Visual Line"]
@@ -27,6 +25,11 @@ class StatusBarManager
[klass, text] = newContents
@element.className = klass
@element.textContent = text
+ else
+ @hide()
+
+ hide: ->
+ @element.className = 'hidden'
# Private
diff --git a/atom/packages/vim-mode/lib/text-objects.coffee b/atom/packages/vim-mode/lib/text-objects.coffee
index 6d9741b..bf38605 100644
--- a/atom/packages/vim-mode/lib/text-objects.coffee
+++ b/atom/packages/vim-mode/lib/text-objects.coffee
@@ -1,5 +1,7 @@
{Range} = require 'atom'
AllWhitespace = /^\s$/
+WholeWordRegex = /\S+/
+{mergeRanges} = require './utils'
class TextObject
constructor: (@editor, @state) ->
@@ -7,11 +9,21 @@ class TextObject
isComplete: -> true
isRecordable: -> false
+ execute: -> @select.apply(this, arguments)
+
class SelectInsideWord extends TextObject
select: ->
- @editor.selectWordsContainingCursors()
+ for selection in @editor.getSelections()
+ selection.expandOverWord()
[true]
+class SelectInsideWholeWord extends TextObject
+ select: ->
+ for selection in @editor.getSelections()
+ range = selection.cursor.getCurrentWordBufferRange({wordRegex: WholeWordRegex})
+ selection.setBufferRange(mergeRanges(selection.getBufferRange(), range))
+ 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.
@@ -76,7 +88,7 @@ class SelectInsideQuotes extends TextObject
++ start.column # skip the opening quote
end = @findClosingQuote(start)
if end?
- selection.setBufferRange([start, end])
+ selection.setBufferRange(mergeRanges(selection.getBufferRange(), [start, end]))
not selection.isEmpty()
# SelectInsideBrackets and the previous class defined (SelectInsideQuotes) are
@@ -126,13 +138,13 @@ class SelectInsideBrackets extends TextObject
++ start.column # skip the opening quote
end = @findClosingBracket(start)
if end?
- selection.setBufferRange([start, end])
+ selection.setBufferRange(mergeRanges(selection.getBufferRange(), [start, end]))
not selection.isEmpty()
class SelectAWord extends TextObject
select: ->
for selection in @editor.getSelections()
- selection.selectWord()
+ selection.expandOverWord()
loop
endPoint = selection.getBufferRange().end
char = @editor.getTextInRange(Range.fromPointWithDelta(endPoint, 0, 1))
@@ -140,25 +152,56 @@ class SelectAWord extends TextObject
selection.selectRight()
true
-class SelectInsideParagraph extends TextObject
- constructor: (@editor, @inclusive) ->
+class SelectAWholeWord extends TextObject
select: ->
for selection in @editor.getSelections()
- range = selection.cursor.getCurrentParagraphBufferRange()
- if range?
- selection.setBufferRange(range)
- selection.selectToBeginningOfNextParagraph()
+ range = selection.cursor.getCurrentWordBufferRange({wordRegex: WholeWordRegex})
+ selection.setBufferRange(mergeRanges(selection.getBufferRange(), range))
+ loop
+ endPoint = selection.getBufferRange().end
+ char = @editor.getTextInRange(Range.fromPointWithDelta(endPoint, 0, 1))
+ break unless AllWhitespace.test(char)
+ selection.selectRight()
true
-class SelectAParagraph extends TextObject
- constructor: (@editor, @inclusive) ->
+class Paragraph extends TextObject
+
select: ->
for selection in @editor.getSelections()
- range = selection.cursor.getCurrentParagraphBufferRange()
- if range?
- selection.setBufferRange(range)
- selection.selectToBeginningOfNextParagraph()
- selection.selectDown()
- true
+ @selectParagraph(selection)
+
+ # Return a range delimted by the start or the end of a paragraph
+ paragraphDelimitedRange: (startPoint) ->
+ inParagraph = @isParagraphLine(@editor.lineTextForBufferRow(startPoint.row))
+ upperRow = @searchLines(startPoint.row, -1, inParagraph)
+ lowerRow = @searchLines(startPoint.row, @editor.getLineCount(), inParagraph)
+ new Range([upperRow + 1, 0], [lowerRow, 0])
+
+ searchLines: (startRow, rowLimit, startedInParagraph) ->
+ for currentRow in [startRow..rowLimit]
+ line = @editor.lineTextForBufferRow(currentRow)
+ if startedInParagraph isnt @isParagraphLine(line)
+ return currentRow
+ rowLimit
+
+ isParagraphLine: (line) -> (/\S/.test(line))
+
+class SelectInsideParagraph extends Paragraph
+ selectParagraph: (selection) ->
+ oldRange = selection.getBufferRange()
+ startPoint = selection.cursor.getBufferPosition()
+ newRange = @paragraphDelimitedRange(startPoint)
+ selection.setBufferRange(mergeRanges(oldRange, newRange))
+ true
+
+class SelectAParagraph extends Paragraph
+ selectParagraph: (selection) ->
+ oldRange = selection.getBufferRange()
+ startPoint = selection.cursor.getBufferPosition()
+ newRange = @paragraphDelimitedRange(startPoint)
+ nextRange = @paragraphDelimitedRange(newRange.end)
+ selection.setBufferRange(mergeRanges(oldRange, [newRange.start, nextRange.end]))
+ true
-module.exports = {TextObject, SelectInsideWord, SelectInsideQuotes, SelectInsideBrackets, SelectAWord, SelectInsideParagraph, SelectAParagraph}
+module.exports = {TextObject, SelectInsideWord, SelectInsideWholeWord, SelectInsideQuotes,
+ SelectInsideBrackets, SelectAWord, SelectAWholeWord, SelectInsideParagraph, SelectAParagraph}
diff --git a/atom/packages/vim-mode/lib/utils.coffee b/atom/packages/vim-mode/lib/utils.coffee
index 362b65d..5a26310 100644
--- a/atom/packages/vim-mode/lib/utils.coffee
+++ b/atom/packages/vim-mode/lib/utils.coffee
@@ -1,3 +1,5 @@
+{Range} = require 'atom'
+
module.exports =
# Public: Determines if a string should be considered linewise or character
#
@@ -12,3 +14,14 @@ module.exports =
'linewise'
else
'character'
+
+ # Public: return a union of two ranges, or simply the newRange if the oldRange is empty.
+ #
+ # Returns a Range
+ mergeRanges: (oldRange, newRange) ->
+ oldRange = Range.fromObject oldRange
+ newRange = Range.fromObject newRange
+ if oldRange.isEmpty()
+ newRange
+ else
+ oldRange.union(newRange)
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
index 8787210..8e63fd2 100644
--- a/atom/packages/vim-mode/lib/view-models/search-view-model.coffee
+++ b/atom/packages/vim-mode/lib/view-models/search-view-model.coffee
@@ -40,3 +40,11 @@ class SearchViewModel extends ViewModel
atom.beep()
super(view)
@vimState.pushSearchHistory(@view.value)
+
+ update: (reverse) ->
+ if reverse
+ @view.classList.add('reverse-search-input')
+ @view.classList.remove('search-input')
+ else
+ @view.classList.add('search-input')
+ @view.classList.remove('reverse-search-input')
diff --git a/atom/packages/vim-mode/lib/view-models/view-model.coffee b/atom/packages/vim-mode/lib/view-models/view-model.coffee
index 9cc63e1..b9da169 100644
--- a/atom/packages/vim-mode/lib/view-models/view-model.coffee
+++ b/atom/packages/vim-mode/lib/view-models/view-model.coffee
@@ -1,10 +1,10 @@
-VimCommandModeInputElement = require './vim-command-mode-input-element'
+VimNormalModeInputElement = require './vim-normal-mode-input-element'
class ViewModel
constructor: (@operation, opts={}) ->
{@editor, @vimState} = @operation
- @view = new VimCommandModeInputElement().initialize(this, opts)
- @editor.commandModeInputView = @view
+ @view = new VimNormalModeInputElement().initialize(this, atom.views.getView(@editor), opts)
+ @editor.normalModeInputView = @view
@vimState.onDidFailToCompose => @view.remove()
confirm: (view) ->
@@ -13,6 +13,7 @@ class ViewModel
cancel: (view) ->
if @vimState.isOperatorPending()
@vimState.pushOperations(new Input(''))
+ delete @editor.normalModeInputView
class Input
constructor: (@characters) ->
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-normal-mode-input-element.coffee
index 4fd0c52..2371753 100644
--- a/atom/packages/vim-mode/lib/view-models/vim-command-mode-input-element.coffee
+++ b/atom/packages/vim-mode/lib/view-models/vim-normal-mode-input-element.coffee
@@ -1,29 +1,25 @@
-class VimCommandModeInputElement extends HTMLDivElement
+class VimNormalModeInputElement extends HTMLDivElement
createdCallback: ->
- @className = "command-mode-input"
+ @className = "normal-mode-input"
- @editorContainer = document.createElement("div")
- @editorContainer.className = "editor-container"
-
- @appendChild(@editorContainer)
-
- initialize: (@viewModel, opts = {}) ->
+ initialize: (@viewModel, @mainEditorElement, opts = {}) ->
if opts.class?
- @editorContainer.classList.add(opts.class)
-
- if opts.hidden
- @editorContainer.style.height = "0px"
+ @classList.add(opts.class)
@editorElement = document.createElement "atom-text-editor"
@editorElement.classList.add('editor')
@editorElement.getModel().setMini(true)
@editorElement.setAttribute('mini', '')
- @editorContainer.appendChild(@editorElement)
+ @appendChild(@editorElement)
@singleChar = opts.singleChar
@defaultText = opts.defaultText ? ''
- @panel = atom.workspace.addBottomPanel(item: this, priority: 100)
+ if opts.hidden
+ @classList.add('vim-hidden-normal-mode-input')
+ @mainEditorElement.parentNode.appendChild(this)
+ else
+ @panel = atom.workspace.addBottomPanel(item: this, priority: 100)
@focus()
@handleEvents()
@@ -32,8 +28,11 @@ class VimCommandModeInputElement extends HTMLDivElement
handleEvents: ->
if @singleChar?
+ compositing = false
@editorElement.getModel().getBuffer().onDidChange (e) =>
- @confirm() if e.newText
+ @confirm() if e.newText and not compositing
+ @editorElement.addEventListener 'compositionstart', -> compositing = true
+ @editorElement.addEventListener 'compositionend', -> compositing = false
else
atom.commands.add(@editorElement, 'editor:newline', @confirm.bind(this))
@@ -55,10 +54,13 @@ class VimCommandModeInputElement extends HTMLDivElement
removePanel: ->
atom.workspace.getActivePane().activate()
- @panel.destroy()
+ if @panel?
+ @panel.destroy()
+ else
+ this.remove()
module.exports =
-document.registerElement("vim-command-mode-input"
+document.registerElement("vim-normal-mode-input"
extends: "div",
- prototype: VimCommandModeInputElement.prototype
+ prototype: VimNormalModeInputElement.prototype
)
diff --git a/atom/packages/vim-mode/lib/vim-mode.coffee b/atom/packages/vim-mode/lib/vim-mode.coffee
index 660fba0..b8ef936 100644
--- a/atom/packages/vim-mode/lib/vim-mode.coffee
+++ b/atom/packages/vim-mode/lib/vim-mode.coffee
@@ -16,7 +16,7 @@ module.exports =
@vimStatesByEditor = new WeakMap
@disposables.add atom.workspace.observeTextEditors (editor) =>
- return if editor.isMini() or @vimStatesByEditor.get(editor)
+ return if editor.isMini() or @getEditorState(editor)
vimState = new VimState(
atom.views.getView(editor),
@@ -28,6 +28,8 @@ module.exports =
@vimStatesByEditor.set(editor, vimState)
vimState.onDidDestroy => @vimStates.delete(vimState)
+ @disposables.add atom.workspace.onDidChangeActivePaneItem @updateToPaneItem.bind(this)
+
@disposables.add new Disposable =>
@vimStates.forEach (vimState) -> vimState.destroy()
@@ -46,6 +48,13 @@ module.exports =
@disposables.add new Disposable =>
@statusBarManager.detach()
+ updateToPaneItem: (item) ->
+ vimState = @getEditorState(item) if item?
+ if vimState?
+ vimState.updateStatusBar()
+ else
+ @statusBarManager.hide()
+
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
index a45551c..db59ee5 100644
--- a/atom/packages/vim-mode/lib/vim-state.coffee
+++ b/atom/packages/vim-mode/lib/vim-state.coffee
@@ -1,3 +1,4 @@
+Grim = require 'grim'
_ = require 'underscore-plus'
{Point, Range} = require 'atom'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
@@ -6,6 +7,7 @@ settings = require './settings'
Operators = require './operators/index'
Prefixes = require './prefixes'
Motions = require './motions/index'
+InsertMode = require './insert-mode'
TextObjects = require './text-objects'
Utils = require './utils'
@@ -18,6 +20,7 @@ class VimState
mode: null
submode: null
destroyed: false
+ replaceModeListener: null
constructor: (@editorElement, @statusBarManager, @globalVimState) ->
@emitter = new Emitter
@@ -28,52 +31,56 @@ class VimState
@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.addEventListener 'mouseup', @checkSelections
+ if atom.commands.onDidDispatch?
+ @subscriptions.add atom.commands.onDidDispatch (e) =>
+ if e.target is @editorElement
+ @checkSelections()
@editorElement.classList.add("vim-mode")
- @setupCommandMode()
+ @setupNormalMode()
if settings.startInInsertMode()
@activateInsertMode()
else
- @activateCommandMode()
+ @activateNormalMode()
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")
+ @editorElement.classList.remove("normal-mode")
+ @editorElement.removeEventListener 'mouseup', @checkSelections
@editor = null
@editorElement = null
+ @emitter.emit 'did-destroy'
# Private: Creates the plugin's bindings
#
# Returns nothing.
- setupCommandMode: ->
+ setupNormalMode: ->
@registerCommands
- 'activate-command-mode': => @activateCommandMode()
+ 'activate-normal-mode': => @activateNormalMode()
'activate-linewise-visual-mode': => @activateVisualMode('linewise')
'activate-characterwise-visual-mode': => @activateVisualMode('characterwise')
'activate-blockwise-visual-mode': => @activateVisualMode('blockwise')
- 'reset-command-mode': => @resetCommandMode()
+ 'reset-normal-mode': => @resetNormalMode()
'repeat-prefix': (e) => @repeatPrefix(e)
'reverse-selections': (e) => @reverseSelections(e)
'undo': (e) => @undo(e)
+ 'replace-mode-backspace': => @replaceModeUndo()
+ 'insert-mode-put': (e) => @insertRegister(@registerName(e))
+ 'copy-from-line-above': => InsertMode.copyCharacterFromAbove(@editor, this)
+ 'copy-from-line-below': => InsertMode.copyCharacterFromBelow(@editor, this)
@registerOperationCommands
'activate-insert-mode': => new Operators.Insert(@editor, this)
- 'substitute': => new Operators.Substitute(@editor, this)
- 'substitute-line': => new Operators.SubstituteLine(@editor, this)
+ 'activate-replace-mode': => new Operators.ReplaceMode(@editor, this)
+ 'substitute': => [new Operators.Change(@editor, this), new Motions.MoveRight(@editor, this)]
+ 'substitute-line': => [new Operators.Change(@editor, this), new Motions.MoveToRelativeLine(@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)
@@ -135,7 +142,10 @@ class VimState
'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)
+ 'scroll-cursor-to-left': => new Scroll.ScrollCursorToLeft(@editorElement)
+ 'scroll-cursor-to-right': => new Scroll.ScrollCursorToRight(@editorElement)
'select-inside-word': => new TextObjects.SelectInsideWord(@editor)
+ 'select-inside-whole-word': => new TextObjects.SelectInsideWholeWord(@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)
@@ -146,6 +156,7 @@ class VimState
'select-inside-parentheses': => new TextObjects.SelectInsideBrackets(@editor, '(', ')', false)
'select-inside-paragraph': => new TextObjects.SelectInsideParagraph(@editor, false)
'select-a-word': => new TextObjects.SelectAWord(@editor)
+ 'select-a-whole-word': => new TextObjects.SelectAWholeWord(@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)
@@ -165,8 +176,8 @@ class VimState
'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?
+ 'repeat-find': (e) => new @globalVimState.currentFind.constructor(@editor, this, repeated: true) if @globalVimState.currentFind
+ 'repeat-find-reverse': (e) => new @globalVimState.currentFind.constructor(@editor, this, repeated: true, reverse: true) if @globalVimState.currentFind
'replace': (e) => new Operators.Replace(@editor, this)
'search': (e) => new Motions.Search(@editor, this)
'reverse-search': (e) => (new Motions.Search(@editor, this)).reversed()
@@ -210,7 +221,7 @@ class VimState
# 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()
+ @resetNormalMode()
@emitter.emit('failed-to-compose')
break
@@ -237,7 +248,7 @@ class VimState
undo: ->
@editor.undo()
- @activateCommandMode()
+ @activateNormalMode()
# Private: Processes the command if the last operation is complete.
#
@@ -247,7 +258,7 @@ class VimState
return
unless @topOperation().isComplete()
- if @mode is 'command' and @topOperation() instanceof Operators.Operator
+ if @mode is 'normal' and @topOperation() instanceof Operators.Operator
@activateOperatorPendingMode()
return
@@ -258,7 +269,7 @@ class VimState
@processOpStack()
catch e
if (e instanceof Operators.OperatorError) or (e instanceof Motions.MotionError)
- @resetCommandMode()
+ @resetNormalMode()
else
throw e
else
@@ -278,6 +289,8 @@ class VimState
# Returns the value of the given register or undefined if it hasn't
# been set.
getRegister: (name) ->
+ if name is '"'
+ name = settings.defaultRegister()
if name in ['*', '+']
text = atom.clipboard.read()
type = Utils.copyType(text)
@@ -312,6 +325,8 @@ class VimState
#
# Returns nothing.
setRegister: (name, value) ->
+ if name is '"'
+ name = settings.defaultRegister()
if name in ['*', '+']
atom.clipboard.write(value.text)
else if name is '_'
@@ -368,43 +383,73 @@ class VimState
# Mode Switching
##############################################################################
- # Private: Used to enable command mode.
+ # Private: Used to enable normal mode.
#
# Returns nothing.
- activateCommandMode: ->
+ activateNormalMode: ->
@deactivateInsertMode()
@deactivateVisualMode()
- @mode = 'command'
+ @mode = 'normal'
@submode = null
- @changeModeClass('command-mode')
+ @changeModeClass('normal-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()
+ @ensureCursorsWithinLine()
@updateStatusBar()
+ # TODO: remove this method and bump the `vim-mode` service version number.
+ activateCommandMode: ->
+ Grim.deprecate("Use ::activateNormalMode instead")
+ @activateNormalMode()
+
# Private: Used to enable insert mode.
#
# Returns nothing.
- activateInsertMode: ->
+ activateInsertMode: (subtype = null) ->
@mode = 'insert'
@editorElement.component.setInputEnabled(true)
@setInsertionCheckpoint()
- @submode = null
+ @submode = subtype
@changeModeClass('insert-mode')
@updateStatusBar()
+ activateReplaceMode: ->
+ @activateInsertMode('replace')
+ @replaceModeCounter = 0
+ @editorElement.classList.add('replace-mode')
+ @subscriptions.add @replaceModeListener = @editor.onWillInsertText @replaceModeInsertHandler
+ @subscriptions.add @replaceModeUndoListener = @editor.onDidInsertText @replaceModeUndoHandler
+
+ replaceModeInsertHandler: (event) =>
+ chars = event.text?.split('') or []
+ selections = @editor.getSelections()
+ for char in chars
+ continue if char is '\n'
+ for selection in selections
+ selection.delete() unless selection.cursor.isAtEndOfLine()
+ return
+
+ replaceModeUndoHandler: (event) =>
+ @replaceModeCounter++
+
+ replaceModeUndo: ->
+ if @replaceModeCounter > 0
+ @editor.undo()
+ @editor.undo()
+ @editor.moveLeft()
+ @replaceModeCounter--
+
setInsertionCheckpoint: ->
@insertionCheckpoint = @editor.createCheckpoint() unless @insertionCheckpoint?
deactivateInsertMode: ->
return unless @mode in [null, 'insert']
@editorElement.component.setInputEnabled(false)
+ @editorElement.classList.remove('replace-mode')
@editor.groupChangesSinceCheckpoint(@insertionCheckpoint)
changes = getChangesSinceCheckpoint(@editor.buffer, @insertionCheckpoint)
item = @inputOperator(@history[0])
@@ -413,6 +458,13 @@ class VimState
item.confirmChanges(changes)
for cursor in @editor.getCursors()
cursor.moveLeft() unless cursor.isAtBeginningOfLine()
+ if @replaceModeListener?
+ @replaceModeListener.dispose()
+ @subscriptions.remove @replaceModeListener
+ @replaceModeListener = null
+ @replaceModeUndoListener.dispose()
+ @subscriptions.remove @replaceModeUndoListener
+ @replaceModeUndoListener = null
deactivateVisualMode: ->
return unless @mode is 'visual'
@@ -440,7 +492,7 @@ class VimState
# * activate-linewise-visual-mode
if @mode is 'visual'
if @submode is type
- @activateCommandMode()
+ @activateNormalMode()
return
@submode = type
@@ -486,25 +538,25 @@ class VimState
activateOperatorPendingMode: ->
@deactivateInsertMode()
@mode = 'operator-pending'
- @submodule = null
+ @submode = null
@changeModeClass('operator-pending-mode')
@updateStatusBar()
changeModeClass: (targetMode) ->
- for mode in ['command-mode', 'insert-mode', 'visual-mode', 'operator-pending-mode']
+ for mode in ['normal-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.
+ # Private: Resets the normal mode back to it's initial state.
#
# Returns nothing.
- resetCommandMode: ->
+ resetNormalMode: ->
@clearOpStack()
@editor.clearSelections()
- @activateCommandMode()
+ @activateNormalMode()
# Private: A generic way to create a Register prefix based on the event.
#
@@ -512,11 +564,19 @@ class VimState
#
# Returns nothing.
registerPrefix: (e) ->
+ new Prefixes.Register(@registerName(e))
+
+ # Private: Gets a register name from a keyboard event
+ #
+ # e - The event
+ #
+ # Returns the name of the register
+ registerName: (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)
+ name
# Private: A generic way to create a Number prefix based on the event.
#
@@ -535,8 +595,8 @@ class VimState
@pushOperations(new Prefixes.Repeat(num))
reverseSelections: ->
+ reversed = not @editor.getLastSelection().isReversed()
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
@@ -581,6 +641,34 @@ class VimState
updateStatusBar: ->
@statusBarManager.update(@mode, @submode)
+ # Private: insert the contents of the register in the editor
+ #
+ # name - the name of the register to insert
+ #
+ # Returns nothing.
+ insertRegister: (name) ->
+ text = @getRegister(name)?.text
+ @editor.insertText(text) if text?
+
+ # Private: ensure the mode follows the state of selections
+ checkSelections: =>
+ return unless @editor?
+ if @editor.getSelections().every((selection) -> selection.isEmpty())
+ @ensureCursorsWithinLine() if @mode is 'normal'
+ @activateNormalMode() if @mode is 'visual'
+ else
+ @activateVisualMode('characterwise') if @mode is 'normal'
+
+ # Private: ensure the cursor stays within the line as appropriate
+ ensureCursorsWithinLine: =>
+ for cursor in @editor.getCursors()
+ {goalColumn} = cursor
+ if cursor.isAtEndOfLine() and not cursor.isAtBeginningOfLine()
+ cursor.moveLeft()
+ cursor.goalColumn = goalColumn
+
+ @editor.mergeCursors()
+
# 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) ->