_ = 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 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) -> return [] if @input.characters is "" 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) @updateViewModel() reversed: => @initiallyReversed = @reverse = true @updateCurrentSearch() @updateViewModel() this updateViewModel: -> @viewModel.update(@initiallyReversed) 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}