1 _ = require 'underscore-plus'
2 {MotionWithInput} = require './general-motions'
3 SearchViewModel = require '../view-models/search-view-model'
4 {Input} = require '../view-models/view-model'
5 {Point, Range} = require 'atom'
6 settings = require '../settings'
8 class SearchBase extends MotionWithInput
9 operatesInclusively: false
11 constructor: (@editor, @vimState, options = {}) ->
12 super(@editor, @vimState)
13 @reverse = @initiallyReversed = false
14 @updateCurrentSearch() unless options.dontUpdateCurrentSearch
17 @initiallyReversed = @reverse = true
18 @updateCurrentSearch()
21 moveCursor: (cursor, count=1) ->
22 ranges = @scan(cursor)
24 range = ranges[(count - 1) % ranges.length]
25 cursor.setBufferPosition(range.start)
30 currentPosition = cursor.getBufferPosition()
32 [rangesBefore, rangesAfter] = [[], []]
33 @editor.scan @getSearchTerm(@input.characters), ({range}) =>
34 isBefore = if @reverse
35 range.start.compare(currentPosition) < 0
37 range.start.compare(currentPosition) <= 0
40 rangesBefore.push(range)
42 rangesAfter.push(range)
45 rangesAfter.concat(rangesBefore).reverse()
47 rangesAfter.concat(rangesBefore)
49 getSearchTerm: (term) ->
50 modifiers = {'g': true}
52 if not term.match('[A-Z]') and settings.useSmartcaseForSearch()
55 if term.indexOf('\\c') >= 0
56 term = term.replace('\\c', '')
59 modFlags = Object.keys(modifiers).join('')
62 new RegExp(term, modFlags)
64 new RegExp(_.escapeRegExp(term), modFlags)
66 updateCurrentSearch: ->
67 @vimState.globalVimState.currentSearch.reverse = @reverse
68 @vimState.globalVimState.currentSearch.initiallyReversed = @initiallyReversed
70 replicateCurrentSearch: ->
71 @reverse = @vimState.globalVimState.currentSearch.reverse
72 @initiallyReversed = @vimState.globalVimState.currentSearch.initiallyReversed
74 class Search extends SearchBase
75 constructor: (@editor, @vimState) ->
76 super(@editor, @vimState)
77 @viewModel = new SearchViewModel(this)
79 class SearchCurrentWord extends SearchBase
82 constructor: (@editor, @vimState) ->
83 super(@editor, @vimState)
85 # FIXME: This must depend on the current language
86 defaultIsKeyword = "[@a-zA-Z0-9_\-]+"
87 userIsKeyword = atom.config.get('vim-mode.iskeyword')
88 @keywordRegex = new RegExp(userIsKeyword or defaultIsKeyword)
90 searchString = @getCurrentWordMatch()
91 @input = new Input(searchString)
92 @vimState.pushSearchHistory(searchString) unless searchString is @vimState.getSearchHistoryItem()
95 cursor = @editor.getLastCursor()
96 wordStart = cursor.getBeginningOfCurrentWordBufferPosition(wordRegex: @keywordRegex, allowPrevious: false)
97 wordEnd = cursor.getEndOfCurrentWordBufferPosition (wordRegex: @keywordRegex, allowNext: false)
98 cursorPosition = cursor.getBufferPosition()
100 if wordEnd.column is cursorPosition.column
101 # either we don't have a current word, or it ends on cursor, i.e. precedes it, so look for the next one
102 wordEnd = cursor.getEndOfCurrentWordBufferPosition (wordRegex: @keywordRegex, allowNext: true)
103 return "" if wordEnd.row isnt cursorPosition.row # don't look beyond the current line
105 cursor.setBufferPosition wordEnd
106 wordStart = cursor.getBeginningOfCurrentWordBufferPosition(wordRegex: @keywordRegex, allowPrevious: false)
108 cursor.setBufferPosition wordStart
110 @editor.getTextInBufferRange([wordStart, wordEnd])
112 cursorIsOnEOF: (cursor) ->
113 pos = cursor.getNextWordBoundaryBufferPosition(wordRegex: @keywordRegex)
114 eofPos = @editor.getEofBufferPosition()
115 pos.row is eofPos.row and pos.column is eofPos.column
117 getCurrentWordMatch: ->
118 characters = @getCurrentWord()
119 if characters.length > 0
120 if /\W/.test(characters) then "#{characters}\\b" else "\\b#{characters}\\b"
126 execute: (count=1) ->
127 super(count) if @input.characters.length > 0
129 OpenBrackets = ['(', '{', '[']
130 CloseBrackets = [')', '}', ']']
131 AnyBracket = new RegExp(OpenBrackets.concat(CloseBrackets).map(_.escapeRegExp).join("|"))
133 class BracketMatchingMotion extends SearchBase
134 operatesInclusively: true
138 searchForMatch: (startPosition, reverse, inCharacter, outCharacter) ->
140 point = startPosition.copy()
141 lineLength = @editor.lineTextForBufferRow(point.row).length
142 eofPosition = @editor.getEofBufferPosition().translate([0, 1])
143 increment = if reverse then -1 else 1
146 character = @characterAt(point)
147 depth++ if character is inCharacter
148 depth-- if character is outCharacter
150 return point if depth is 0
152 point.column += increment
154 return null if depth < 0
155 return null if point.isEqual([0, -1])
156 return null if point.isEqual(eofPosition)
160 lineLength = @editor.lineTextForBufferRow(point.row).length
161 point.column = lineLength - 1
162 else if point.column >= lineLength
164 lineLength = @editor.lineTextForBufferRow(point.row).length
167 characterAt: (position) ->
168 @editor.getTextInBufferRange([position, position.translate([0, 1])])
170 getSearchData: (position) ->
171 character = @characterAt(position)
172 if (index = OpenBrackets.indexOf(character)) >= 0
173 [character, CloseBrackets[index], false]
174 else if (index = CloseBrackets.indexOf(character)) >= 0
175 [character, OpenBrackets[index], true]
179 moveCursor: (cursor) ->
180 startPosition = cursor.getBufferPosition()
182 [inCharacter, outCharacter, reverse] = @getSearchData(startPosition)
185 restOfLine = [startPosition, [startPosition.row, Infinity]]
186 @editor.scanInBufferRange AnyBracket, restOfLine, ({range, stop}) ->
187 startPosition = range.start
190 [inCharacter, outCharacter, reverse] = @getSearchData(startPosition)
192 return unless inCharacter?
194 if matchPosition = @searchForMatch(startPosition, reverse, inCharacter, outCharacter)
195 cursor.setBufferPosition(matchPosition)
197 class RepeatSearch extends SearchBase
198 constructor: (@editor, @vimState) ->
199 super(@editor, @vimState, dontUpdateCurrentSearch: true)
200 @input = new Input(@vimState.getSearchHistoryItem(0) ? "")
201 @replicateCurrentSearch()
206 @reverse = not @initiallyReversed
210 module.exports = {Search, SearchCurrentWord, BracketMatchingMotion, RepeatSearch}