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