]> git.r.bdr.sh - rbdr/dotfiles/blame - atom/packages/vim-mode/lib/motions/general-motions.coffee
Adds atom
[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
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
113class 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
126class 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
141class 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
149class 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
164class 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
173class 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
182class MoveToPreviousWord extends Motion
183 operatesInclusively: false
184
185 moveCursor: (cursor, count=1) ->
186 _.times count, ->
187 cursor.moveToBeginningOfWord()
188
189class 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
206class 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
235class MoveToNextWholeWord extends MoveToNextWord
236 wordRegex: WholeWordOrEmptyLineRegex
237
238class 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
259class MoveToEndOfWholeWord extends MoveToEndOfWord
260 wordRegex: WholeWordRegex
261
262class MoveToNextParagraph extends Motion
263 operatesInclusively: false
264
265 moveCursor: (cursor, count=1) ->
266 _.times count, ->
267 cursor.moveToBeginningOfNextParagraph()
268
269class MoveToPreviousParagraph extends Motion
270 moveCursor: (cursor, count=1) ->
271 _.times count, ->
272 cursor.moveToBeginningOfPreviousParagraph()
273
274class MoveToLine extends Motion
275 operatesLinewise: true
276
277 getDestinationRow: (count) ->
278 if count? then count - 1 else (@editor.getLineCount() - 1)
279
280class 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
286class 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
293class 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
302class MoveToBeginningOfLine extends Motion
303 operatesInclusively: false
304
305 moveCursor: (cursor, count=1) ->
306 _.times count, ->
307 cursor.moveToBeginningOfLine()
308
309class MoveToFirstCharacterOfLine extends Motion
310 operatesInclusively: false
311
312 moveCursor: (cursor, count=1) ->
313 _.times count, ->
314 cursor.moveToBeginningOfLine()
315 cursor.moveToFirstCharacterOfLine()
316
317class 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
327class 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
336class 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
355class 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
365class 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
374class 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
381class 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
390class 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
400class 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
407class 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
438class ScrollHalfUpKeepCursor extends ScrollKeepingCursor
439 scrollDestination: (count) ->
440 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
441 @editor.getScrollTop() - count * half
442
443class ScrollFullUpKeepCursor extends ScrollKeepingCursor
444 scrollDestination: (count) ->
445 @editor.getScrollTop() - (count * @editor.getHeight())
446
447class ScrollHalfDownKeepCursor extends ScrollKeepingCursor
448 scrollDestination: (count) ->
449 half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
450 @editor.getScrollTop() + count * half
451
452class ScrollFullDownKeepCursor extends ScrollKeepingCursor
453 scrollDestination: (count) ->
454 @editor.getScrollTop() + (count * @editor.getHeight())
455
456module.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}