From 24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a Mon Sep 17 00:00:00 2001 From: Ben Beltran Date: Fri, 10 Jul 2015 11:12:25 -0500 Subject: Adds atom --- .../vim-mode/lib/motions/find-motion.coffee | 54 +++ .../vim-mode/lib/motions/general-motions.coffee | 466 +++++++++++++++++++++ atom/packages/vim-mode/lib/motions/index.coffee | 14 + .../lib/motions/move-to-mark-motion.coffee | 25 ++ .../vim-mode/lib/motions/search-motion.coffee | 210 ++++++++++ 5 files changed, 769 insertions(+) create mode 100644 atom/packages/vim-mode/lib/motions/find-motion.coffee create mode 100644 atom/packages/vim-mode/lib/motions/general-motions.coffee create mode 100644 atom/packages/vim-mode/lib/motions/index.coffee create mode 100644 atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee create mode 100644 atom/packages/vim-mode/lib/motions/search-motion.coffee (limited to 'atom/packages/vim-mode/lib/motions') 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} -- cgit