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