diff options
| author | Ben Beltran <ben@nsovocal.com> | 2015-09-28 12:39:59 -0500 |
|---|---|---|
| committer | Ben Beltran <ben@nsovocal.com> | 2015-09-28 12:39:59 -0500 |
| commit | 455f099b504ec07dd492ab31987ae05a6f6774c7 (patch) | |
| tree | 22b0c7fd8e492733b97e661750a2d79c46da451d /atom/packages/vim-mode/lib | |
| parent | 24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a (diff) | |
Adds atom packages
Diffstat (limited to 'atom/packages/vim-mode/lib')
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) -> |