]> git.r.bdr.sh - rbdr/dotfiles/blob - atom/packages/vim-mode/lib/motions/general-motions.coffee
b5cd627225ca832f6ca25a6c182b09c868c27132
[rbdr/dotfiles] / atom / packages / vim-mode / lib / motions / general-motions.coffee
1 _ = require 'underscore-plus'
2 {Point, Range} = require 'atom'
3 settings = require '../settings'
4
5 WholeWordRegex = /\S+/
6 WholeWordOrEmptyLineRegex = /^\s*$|\S+/
7 AllWhitespace = /^\s$/
8
9 class MotionError
10 constructor: (@message) ->
11 @name = 'Motion Error'
12
13 class Motion
14 operatesInclusively: false
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)
23 else if @vimState.mode is 'visual'
24 @moveSelectionVisual(selection, count, options)
25 else if @operatesInclusively
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) ->
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) ->
81 selection.modifySelection =>
82 range = selection.getBufferRange()
83 [oldStart, oldEnd] = [range.start, range.end]
84
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
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
94 # put cursor back after the last character so it is also selected
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
103 # if we reversed or emptied a normal selection
104 # we need to select again the last character deselected above the motion
105 if (isReversed or isEmpty) and not (wasReversed or wasEmpty)
106 selection.setBufferRange([newStart, [newEnd.row, oldStart.column + 1]])
107
108 # if we re-reversed a reversed non-empty selection,
109 # we need to keep the last character of the old selection selected
110 if wasReversed and not wasEmpty and not isReversed
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)
118
119 moveSelection: (selection, count, options) ->
120 selection.modifySelection => @moveCursor(selection.cursor, count, options)
121
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
132 class CurrentSelection extends Motion
133 constructor: (@editor, @vimState) ->
134 super(@editor, @vimState)
135 @lastSelectionRange = @editor.getSelectedBufferRange()
136 @wasLinewise = @isLinewise()
137
138 execute: (count=1) ->
139 _.times(count, -> true)
140
141 select: (count=1) ->
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
150 _.times(count, -> true)
151
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
167 # Public: Generic class for motions that require extra input
168 class 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
183 class MoveLeft extends Motion
184 moveCursor: (cursor, count=1) ->
185 _.times count, ->
186 cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion()
187
188 class MoveRight extends Motion
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()
199
200 class MoveUp extends Motion
201 operatesLinewise: true
202
203 moveCursor: (cursor, count=1) ->
204 _.times count, ->
205 unless cursor.getScreenRow() is 0
206 cursor.moveUp()
207
208 class 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()
215
216 class MoveToPreviousWord extends Motion
217 moveCursor: (cursor, count=1) ->
218 _.times count, ->
219 cursor.moveToBeginningOfWord()
220
221 class MoveToPreviousWholeWord extends Motion
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
236 class MoveToNextWord extends Motion
237 wordRegex: null
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
264 class MoveToNextWholeWord extends MoveToNextWord
265 wordRegex: WholeWordOrEmptyLineRegex
266
267 class MoveToEndOfWord extends Motion
268 operatesInclusively: true
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
289 class MoveToEndOfWholeWord extends MoveToEndOfWord
290 wordRegex: WholeWordRegex
291
292 class MoveToNextParagraph extends Motion
293 moveCursor: (cursor, count=1) ->
294 _.times count, ->
295 cursor.moveToBeginningOfNextParagraph()
296
297 class MoveToPreviousParagraph extends Motion
298 moveCursor: (cursor, count=1) ->
299 _.times count, ->
300 cursor.moveToBeginningOfPreviousParagraph()
301
302 class MoveToLine extends Motion
303 operatesLinewise: true
304
305 getDestinationRow: (count) ->
306 if count? then count - 1 else (@editor.getLineCount() - 1)
307
308 class 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
314 class MoveToRelativeLine extends MoveToLine
315 moveCursor: (cursor, count=1) ->
316 {row, column} = cursor.getBufferPosition()
317 cursor.setBufferPosition([row + (count - 1), 0])
318
319 class 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
328 class MoveToBeginningOfLine extends Motion
329 moveCursor: (cursor, count=1) ->
330 _.times count, ->
331 cursor.moveToBeginningOfLine()
332
333 class MoveToFirstCharacterOfLine extends Motion
334 moveCursor: (cursor, count=1) ->
335 _.times count, ->
336 cursor.moveToBeginningOfLine()
337 cursor.moveToFirstCharacterOfLine()
338
339 class MoveToFirstCharacterOfLineAndDown extends Motion
340 operatesLinewise: true
341
342 moveCursor: (cursor, count=1) ->
343 _.times count-1, ->
344 cursor.moveDown()
345 cursor.moveToBeginningOfLine()
346 cursor.moveToFirstCharacterOfLine()
347
348 class MoveToLastCharacterOfLine extends Motion
349 moveCursor: (cursor, count=1) ->
350 _.times count, ->
351 cursor.moveToEndOfLine()
352 cursor.goalColumn = Infinity
353
354 class 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
373 class MoveToFirstCharacterOfLineUp extends Motion
374 operatesLinewise: true
375
376 moveCursor: (cursor, count=1) ->
377 _.times count, ->
378 cursor.moveUp()
379 cursor.moveToBeginningOfLine()
380 cursor.moveToFirstCharacterOfLine()
381
382 class 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
391 class 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
398 class 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
407 class 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
417 class MoveToMiddleOfScreen extends MoveToScreenLine
418 getDestinationRow: ->
419 firstScreenRow = @editorElement.getFirstVisibleScreenRow()
420 lastScreenRow = @editorElement.getLastVisibleScreenRow()
421 height = lastScreenRow - firstScreenRow
422 Math.floor(firstScreenRow + (height / 2))
423
424 class 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
448 scrollScreen: (count=1) ->
449 @previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
450 destination = @scrollDestination(count)
451 @editor.setScrollTop(destination)
452 @currentFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
453 destination
454
455 class ScrollHalfUpKeepCursor extends ScrollKeepingCursor
456 scrollDestination: (count) ->
457 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
458 @editor.getScrollTop() - count * half
459
460 class ScrollFullUpKeepCursor extends ScrollKeepingCursor
461 scrollDestination: (count) ->
462 @editor.getScrollTop() - (count * @editor.getHeight())
463
464 class ScrollHalfDownKeepCursor extends ScrollKeepingCursor
465 scrollDestination: (count) ->
466 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
467 @editor.getScrollTop() + count * half
468
469 class ScrollFullDownKeepCursor extends ScrollKeepingCursor
470 scrollDestination: (count) ->
471 @editor.getScrollTop() + (count * @editor.getHeight())
472
473 module.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 }