]> git.r.bdr.sh - rbdr/dotfiles/blame - atom/packages/vim-mode/lib/motions/general-motions.coffee
Update vim plugins
[rbdr/dotfiles] / atom / packages / vim-mode / lib / motions / general-motions.coffee
CommitLineData
24c7594d
BB
1_ = require 'underscore-plus'
2{Point, Range} = require 'atom'
3settings = require '../settings'
4
5WholeWordRegex = /\S+/
6WholeWordOrEmptyLineRegex = /^\s*$|\S+/
7AllWhitespace = /^\s$/
8
9class MotionError
10 constructor: (@message) ->
11 @name = 'Motion Error'
12
13class Motion
455f099b 14 operatesInclusively: false
24c7594d
BB
15 operatesLinewise: false
16
17 constructor: (@editor, @vimState) ->
18
19 select: (count, options) ->
20 value = for selection in @editor.getSelections()
21 if @isLinewise()
22 @moveSelectionLinewise(selection, count, options)
455f099b
BB
23 else if @vimState.mode is 'visual'
24 @moveSelectionVisual(selection, count, options)
25 else if @operatesInclusively
24c7594d
BB
26 @moveSelectionInclusively(selection, count, options)
27 else
28 @moveSelection(selection, count, options)
29 not selection.isEmpty()
30
31 @editor.mergeCursors()
32 @editor.mergeIntersectingSelections()
33 value
34
35 execute: (count) ->
36 for cursor in @editor.getCursors()
37 @moveCursor(cursor, count)
38 @editor.mergeCursors()
39
40 moveSelectionLinewise: (selection, count, options) ->
41 selection.modifySelection =>
42 [oldStartRow, oldEndRow] = selection.getBufferRowRange()
43
44 wasEmpty = selection.isEmpty()
45 wasReversed = selection.isReversed()
46 unless wasEmpty or wasReversed
47 selection.cursor.moveLeft()
48
49 @moveCursor(selection.cursor, count, options)
50
51 isEmpty = selection.isEmpty()
52 isReversed = selection.isReversed()
53 unless isEmpty or isReversed
54 selection.cursor.moveRight()
55
56 [newStartRow, newEndRow] = selection.getBufferRowRange()
57
58 if isReversed and not wasReversed
59 newEndRow = Math.max(newEndRow, oldStartRow)
60 if wasReversed and not isReversed
61 newStartRow = Math.min(newStartRow, oldEndRow)
62
63 selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]])
64
65 moveSelectionInclusively: (selection, count, options) ->
455f099b
BB
66 return @moveSelectionVisual(selection, count, options) unless selection.isEmpty()
67
68 selection.modifySelection =>
69 @moveCursor(selection.cursor, count, options)
70 return if selection.isEmpty()
71
72 if selection.isReversed()
73 # for backward motion, add the original starting character of the motion
74 {start, end} = selection.getBufferRange()
75 selection.setBufferRange([start, [end.row, end.column + 1]])
76 else
77 # for forward motion, add the ending character of the motion
78 selection.cursor.moveRight()
79
80 moveSelectionVisual: (selection, count, options) ->
24c7594d
BB
81 selection.modifySelection =>
82 range = selection.getBufferRange()
83 [oldStart, oldEnd] = [range.start, range.end]
84
455f099b
BB
85 # in visual mode, atom cursor is after the last selected character,
86 # so here put cursor in the expected place for the following motion
24c7594d
BB
87 wasEmpty = selection.isEmpty()
88 wasReversed = selection.isReversed()
89 unless wasEmpty or wasReversed
90 selection.cursor.moveLeft()
91
92 @moveCursor(selection.cursor, count, options)
93
455f099b 94 # put cursor back after the last character so it is also selected
24c7594d
BB
95 isEmpty = selection.isEmpty()
96 isReversed = selection.isReversed()
97 unless isEmpty or isReversed
98 selection.cursor.moveRight()
99
100 range = selection.getBufferRange()
101 [newStart, newEnd] = [range.start, range.end]
102
455f099b
BB
103 # if we reversed or emptied a normal selection
104 # we need to select again the last character deselected above the motion
24c7594d
BB
105 if (isReversed or isEmpty) and not (wasReversed or wasEmpty)
106 selection.setBufferRange([newStart, [newEnd.row, oldStart.column + 1]])
455f099b
BB
107
108 # if we re-reversed a reversed non-empty selection,
109 # we need to keep the last character of the old selection selected
24c7594d 110 if wasReversed and not wasEmpty and not isReversed
455f099b
BB
111 selection.setBufferRange([[oldEnd.row, oldEnd.column - 1], newEnd])
112
113 # keep a single-character selection non-reversed
114 range = selection.getBufferRange()
115 [newStart, newEnd] = [range.start, range.end]
116 if selection.isReversed() and newStart.row is newEnd.row and newStart.column + 1 is newEnd.column
117 selection.setBufferRange(range, reversed: false)
24c7594d
BB
118
119 moveSelection: (selection, count, options) ->
120 selection.modifySelection => @moveCursor(selection.cursor, count, options)
121
24c7594d
BB
122 isComplete: -> true
123
124 isRecordable: -> false
125
126 isLinewise: ->
127 if @vimState?.mode is 'visual'
128 @vimState?.submode is 'linewise'
129 else
130 @operatesLinewise
131
24c7594d
BB
132class CurrentSelection extends Motion
133 constructor: (@editor, @vimState) ->
134 super(@editor, @vimState)
455f099b
BB
135 @lastSelectionRange = @editor.getSelectedBufferRange()
136 @wasLinewise = @isLinewise()
24c7594d
BB
137
138 execute: (count=1) ->
139 _.times(count, -> true)
140
141 select: (count=1) ->
455f099b
BB
142 # in visual mode, the current selections are already there
143 # if we're not in visual mode, we are repeating some operation and need to re-do the selections
144 unless @vimState.mode is 'visual'
145 if @wasLinewise
146 @selectLines()
147 else
148 @selectCharacters()
149
24c7594d
BB
150 _.times(count, -> true)
151
455f099b
BB
152 selectLines: ->
153 lastSelectionExtent = @lastSelectionRange.getExtent()
154 for selection in @editor.getSelections()
155 cursor = selection.cursor.getBufferPosition()
156 selection.setBufferRange [[cursor.row, 0], [cursor.row + lastSelectionExtent.row, 0]]
157 return
158
159 selectCharacters: ->
160 lastSelectionExtent = @lastSelectionRange.getExtent()
161 for selection in @editor.getSelections()
162 {start} = selection.getBufferRange()
163 newEnd = start.traverse(lastSelectionExtent)
164 selection.setBufferRange([start, newEnd])
165 return
166
24c7594d
BB
167# Public: Generic class for motions that require extra input
168class MotionWithInput extends Motion
169 constructor: (@editor, @vimState) ->
170 super(@editor, @vimState)
171 @complete = false
172
173 isComplete: -> @complete
174
175 canComposeWith: (operation) -> return operation.characters?
176
177 compose: (input) ->
178 if not input.characters
179 throw new MotionError('Must compose with an Input')
180 @input = input
181 @complete = true
182
183class MoveLeft extends Motion
24c7594d 184 moveCursor: (cursor, count=1) ->
455f099b 185 _.times count, ->
24c7594d 186 cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion()
24c7594d
BB
187
188class MoveRight extends Motion
24c7594d
BB
189 moveCursor: (cursor, count=1) ->
190 _.times count, =>
191 wrapToNextLine = settings.wrapLeftRightMotion()
192
193 # when the motion is combined with an operator, we will only wrap to the next line
194 # if we are already at the end of the line (after the last character)
195 wrapToNextLine = false if @vimState.mode is 'operator-pending' and not cursor.isAtEndOfLine()
196
197 cursor.moveRight() unless cursor.isAtEndOfLine()
198 cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine()
24c7594d
BB
199
200class MoveUp extends Motion
201 operatesLinewise: true
202
203 moveCursor: (cursor, count=1) ->
455f099b 204 _.times count, ->
24c7594d
BB
205 unless cursor.getScreenRow() is 0
206 cursor.moveUp()
24c7594d
BB
207
208class MoveDown extends Motion
209 operatesLinewise: true
210
211 moveCursor: (cursor, count=1) ->
212 _.times count, =>
213 unless cursor.getScreenRow() is @editor.getLastScreenRow()
214 cursor.moveDown()
24c7594d
BB
215
216class MoveToPreviousWord extends Motion
24c7594d
BB
217 moveCursor: (cursor, count=1) ->
218 _.times count, ->
219 cursor.moveToBeginningOfWord()
220
221class MoveToPreviousWholeWord extends Motion
24c7594d
BB
222 moveCursor: (cursor, count=1) ->
223 _.times count, =>
224 cursor.moveToBeginningOfWord()
225 while not @isWholeWord(cursor) and not @isBeginningOfFile(cursor)
226 cursor.moveToBeginningOfWord()
227
228 isWholeWord: (cursor) ->
229 char = cursor.getCurrentWordPrefix().slice(-1)
230 AllWhitespace.test(char)
231
232 isBeginningOfFile: (cursor) ->
233 cur = cursor.getBufferPosition()
234 not cur.row and not cur.column
235
236class MoveToNextWord extends Motion
237 wordRegex: null
24c7594d
BB
238
239 moveCursor: (cursor, count=1, options) ->
240 _.times count, =>
241 current = cursor.getBufferPosition()
242
243 next = if options?.excludeWhitespace
244 cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
245 else
246 cursor.getBeginningOfNextWordBufferPosition(wordRegex: @wordRegex)
247
248 return if @isEndOfFile(cursor)
249
250 if cursor.isAtEndOfLine()
251 cursor.moveDown()
252 cursor.moveToBeginningOfLine()
253 cursor.skipLeadingWhitespace()
254 else if current.row is next.row and current.column is next.column
255 cursor.moveToEndOfWord()
256 else
257 cursor.setBufferPosition(next)
258
259 isEndOfFile: (cursor) ->
260 cur = cursor.getBufferPosition()
261 eof = @editor.getEofBufferPosition()
262 cur.row is eof.row and cur.column is eof.column
263
264class MoveToNextWholeWord extends MoveToNextWord
265 wordRegex: WholeWordOrEmptyLineRegex
266
267class MoveToEndOfWord extends Motion
455f099b 268 operatesInclusively: true
24c7594d
BB
269 wordRegex: null
270
271 moveCursor: (cursor, count=1) ->
272 _.times count, =>
273 current = cursor.getBufferPosition()
274
275 next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
276 next.column-- if next.column > 0
277
278 if next.isEqual(current)
279 cursor.moveRight()
280 if cursor.isAtEndOfLine()
281 cursor.moveDown()
282 cursor.moveToBeginningOfLine()
283
284 next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
285 next.column-- if next.column > 0
286
287 cursor.setBufferPosition(next)
288
289class MoveToEndOfWholeWord extends MoveToEndOfWord
290 wordRegex: WholeWordRegex
291
292class MoveToNextParagraph extends Motion
24c7594d
BB
293 moveCursor: (cursor, count=1) ->
294 _.times count, ->
295 cursor.moveToBeginningOfNextParagraph()
296
297class MoveToPreviousParagraph extends Motion
298 moveCursor: (cursor, count=1) ->
299 _.times count, ->
300 cursor.moveToBeginningOfPreviousParagraph()
301
302class MoveToLine extends Motion
303 operatesLinewise: true
304
305 getDestinationRow: (count) ->
306 if count? then count - 1 else (@editor.getLineCount() - 1)
307
308class MoveToAbsoluteLine extends MoveToLine
309 moveCursor: (cursor, count) ->
310 cursor.setBufferPosition([@getDestinationRow(count), Infinity])
311 cursor.moveToFirstCharacterOfLine()
312 cursor.moveToEndOfLine() if cursor.getBufferColumn() is 0
313
314class MoveToRelativeLine extends MoveToLine
24c7594d
BB
315 moveCursor: (cursor, count=1) ->
316 {row, column} = cursor.getBufferPosition()
317 cursor.setBufferPosition([row + (count - 1), 0])
318
319class MoveToScreenLine extends MoveToLine
320 constructor: (@editorElement, @vimState, @scrolloff) ->
321 @scrolloff = 2 # atom default
322 super(@editorElement.getModel(), @vimState)
323
324 moveCursor: (cursor, count=1) ->
325 {row, column} = cursor.getBufferPosition()
326 cursor.setScreenPosition([@getDestinationRow(count), 0])
327
328class MoveToBeginningOfLine extends Motion
24c7594d
BB
329 moveCursor: (cursor, count=1) ->
330 _.times count, ->
331 cursor.moveToBeginningOfLine()
332
333class MoveToFirstCharacterOfLine extends Motion
24c7594d
BB
334 moveCursor: (cursor, count=1) ->
335 _.times count, ->
336 cursor.moveToBeginningOfLine()
337 cursor.moveToFirstCharacterOfLine()
338
339class MoveToFirstCharacterOfLineAndDown extends Motion
340 operatesLinewise: true
24c7594d 341
455f099b 342 moveCursor: (cursor, count=1) ->
24c7594d
BB
343 _.times count-1, ->
344 cursor.moveDown()
345 cursor.moveToBeginningOfLine()
346 cursor.moveToFirstCharacterOfLine()
347
348class MoveToLastCharacterOfLine extends Motion
24c7594d 349 moveCursor: (cursor, count=1) ->
455f099b 350 _.times count, ->
24c7594d
BB
351 cursor.moveToEndOfLine()
352 cursor.goalColumn = Infinity
24c7594d
BB
353
354class MoveToLastNonblankCharacterOfLineAndDown extends Motion
355 operatesInclusively: true
356
357 # moves cursor to the last non-whitespace character on the line
358 # similar to skipLeadingWhitespace() in atom's cursor.coffee
359 skipTrailingWhitespace: (cursor) ->
360 position = cursor.getBufferPosition()
361 scanRange = cursor.getCurrentLineBufferRange()
362 startOfTrailingWhitespace = [scanRange.end.row, scanRange.end.column - 1]
363 @editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) ->
364 startOfTrailingWhitespace = range.start
365 startOfTrailingWhitespace.column -= 1
366 cursor.setBufferPosition(startOfTrailingWhitespace)
367
368 moveCursor: (cursor, count=1) ->
369 _.times count-1, ->
370 cursor.moveDown()
371 @skipTrailingWhitespace(cursor)
372
373class MoveToFirstCharacterOfLineUp extends Motion
374 operatesLinewise: true
24c7594d
BB
375
376 moveCursor: (cursor, count=1) ->
377 _.times count, ->
378 cursor.moveUp()
379 cursor.moveToBeginningOfLine()
380 cursor.moveToFirstCharacterOfLine()
381
382class MoveToFirstCharacterOfLineDown extends Motion
383 operatesLinewise: true
384
385 moveCursor: (cursor, count=1) ->
386 _.times count, ->
387 cursor.moveDown()
388 cursor.moveToBeginningOfLine()
389 cursor.moveToFirstCharacterOfLine()
390
391class MoveToStartOfFile extends MoveToLine
392 moveCursor: (cursor, count=1) ->
393 {row, column} = @editor.getCursorBufferPosition()
394 cursor.setBufferPosition([@getDestinationRow(count), 0])
395 unless @isLinewise()
396 cursor.moveToFirstCharacterOfLine()
397
398class MoveToTopOfScreen extends MoveToScreenLine
399 getDestinationRow: (count=0) ->
400 firstScreenRow = @editorElement.getFirstVisibleScreenRow()
401 if firstScreenRow > 0
402 offset = Math.max(count - 1, @scrolloff)
403 else
404 offset = if count > 0 then count - 1 else count
405 firstScreenRow + offset
406
407class MoveToBottomOfScreen extends MoveToScreenLine
408 getDestinationRow: (count=0) ->
409 lastScreenRow = @editorElement.getLastVisibleScreenRow()
410 lastRow = @editor.getBuffer().getLastRow()
411 if lastScreenRow isnt lastRow
412 offset = Math.max(count - 1, @scrolloff)
413 else
414 offset = if count > 0 then count - 1 else count
415 lastScreenRow - offset
416
417class MoveToMiddleOfScreen extends MoveToScreenLine
455f099b 418 getDestinationRow: ->
24c7594d
BB
419 firstScreenRow = @editorElement.getFirstVisibleScreenRow()
420 lastScreenRow = @editorElement.getLastVisibleScreenRow()
421 height = lastScreenRow - firstScreenRow
422 Math.floor(firstScreenRow + (height / 2))
423
424class ScrollKeepingCursor extends MoveToLine
425 previousFirstScreenRow: 0
426 currentFirstScreenRow: 0
427
428 constructor: (@editorElement, @vimState) ->
429 super(@editorElement.getModel(), @vimState)
430
431 select: (count, options) ->
432 finalDestination = @scrollScreen(count)
433 super(count, options)
434 @editor.setScrollTop(finalDestination)
435
436 execute: (count) ->
437 finalDestination = @scrollScreen(count)
438 super(count)
439 @editor.setScrollTop(finalDestination)
440
441 moveCursor: (cursor, count=1) ->
442 cursor.setScreenPosition([@getDestinationRow(count), 0])
443
444 getDestinationRow: (count) ->
445 {row, column} = @editor.getCursorScreenPosition()
446 @currentFirstScreenRow - @previousFirstScreenRow + row
447
455f099b 448 scrollScreen: (count=1) ->
24c7594d
BB
449 @previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
450 destination = @scrollDestination(count)
451 @editor.setScrollTop(destination)
452 @currentFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
453 destination
454
455class ScrollHalfUpKeepCursor extends ScrollKeepingCursor
456 scrollDestination: (count) ->
457 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
458 @editor.getScrollTop() - count * half
459
460class ScrollFullUpKeepCursor extends ScrollKeepingCursor
461 scrollDestination: (count) ->
462 @editor.getScrollTop() - (count * @editor.getHeight())
463
464class ScrollHalfDownKeepCursor extends ScrollKeepingCursor
465 scrollDestination: (count) ->
466 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
467 @editor.getScrollTop() + count * half
468
469class ScrollFullDownKeepCursor extends ScrollKeepingCursor
470 scrollDestination: (count) ->
471 @editor.getScrollTop() + (count * @editor.getHeight())
472
473module.exports = {
474 Motion, MotionWithInput, CurrentSelection, MoveLeft, MoveRight, MoveUp, MoveDown,
475 MoveToPreviousWord, MoveToPreviousWholeWord, MoveToNextWord, MoveToNextWholeWord,
476 MoveToEndOfWord, MoveToNextParagraph, MoveToPreviousParagraph, MoveToAbsoluteLine, MoveToRelativeLine, MoveToBeginningOfLine,
477 MoveToFirstCharacterOfLineUp, MoveToFirstCharacterOfLineDown,
478 MoveToFirstCharacterOfLine, MoveToFirstCharacterOfLineAndDown, MoveToLastCharacterOfLine,
479 MoveToLastNonblankCharacterOfLineAndDown, MoveToStartOfFile,
480 MoveToTopOfScreen, MoveToBottomOfScreen, MoveToMiddleOfScreen, MoveToEndOfWholeWord, MotionError,
481 ScrollHalfUpKeepCursor, ScrollFullUpKeepCursor,
482 ScrollHalfDownKeepCursor, ScrollFullDownKeepCursor
483}