]> git.r.bdr.sh - rbdr/dotfiles/blame - atom/packages/vim-mode/lib/vim-state.coffee
Update vim plugins
[rbdr/dotfiles] / atom / packages / vim-mode / lib / vim-state.coffee
CommitLineData
455f099b 1Grim = require 'grim'
24c7594d
BB
2_ = require 'underscore-plus'
3{Point, Range} = require 'atom'
4{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
5settings = require './settings'
6
7Operators = require './operators/index'
8Prefixes = require './prefixes'
9Motions = require './motions/index'
455f099b 10InsertMode = require './insert-mode'
24c7594d
BB
11
12TextObjects = require './text-objects'
13Utils = require './utils'
14Scroll = require './scroll'
15
16module.exports =
17class VimState
18 editor: null
19 opStack: null
20 mode: null
21 submode: null
22 destroyed: false
455f099b 23 replaceModeListener: null
24c7594d
BB
24
25 constructor: (@editorElement, @statusBarManager, @globalVimState) ->
26 @emitter = new Emitter
27 @subscriptions = new CompositeDisposable
28 @editor = @editorElement.getModel()
29 @opStack = []
30 @history = []
31 @marks = {}
32 @subscriptions.add @editor.onDidDestroy => @destroy()
33
455f099b
BB
34 @editorElement.addEventListener 'mouseup', @checkSelections
35 if atom.commands.onDidDispatch?
36 @subscriptions.add atom.commands.onDidDispatch (e) =>
37 if e.target is @editorElement
38 @checkSelections()
24c7594d
BB
39
40 @editorElement.classList.add("vim-mode")
455f099b 41 @setupNormalMode()
24c7594d
BB
42 if settings.startInInsertMode()
43 @activateInsertMode()
44 else
455f099b 45 @activateNormalMode()
24c7594d
BB
46
47 destroy: ->
48 unless @destroyed
49 @destroyed = true
24c7594d
BB
50 @subscriptions.dispose()
51 if @editor.isAlive()
52 @deactivateInsertMode()
53 @editorElement.component?.setInputEnabled(true)
54 @editorElement.classList.remove("vim-mode")
455f099b
BB
55 @editorElement.classList.remove("normal-mode")
56 @editorElement.removeEventListener 'mouseup', @checkSelections
24c7594d
BB
57 @editor = null
58 @editorElement = null
455f099b 59 @emitter.emit 'did-destroy'
24c7594d
BB
60
61 # Private: Creates the plugin's bindings
62 #
63 # Returns nothing.
455f099b 64 setupNormalMode: ->
24c7594d 65 @registerCommands
455f099b 66 'activate-normal-mode': => @activateNormalMode()
24c7594d
BB
67 'activate-linewise-visual-mode': => @activateVisualMode('linewise')
68 'activate-characterwise-visual-mode': => @activateVisualMode('characterwise')
69 'activate-blockwise-visual-mode': => @activateVisualMode('blockwise')
455f099b 70 'reset-normal-mode': => @resetNormalMode()
24c7594d
BB
71 'repeat-prefix': (e) => @repeatPrefix(e)
72 'reverse-selections': (e) => @reverseSelections(e)
73 'undo': (e) => @undo(e)
455f099b
BB
74 'replace-mode-backspace': => @replaceModeUndo()
75 'insert-mode-put': (e) => @insertRegister(@registerName(e))
76 'copy-from-line-above': => InsertMode.copyCharacterFromAbove(@editor, this)
77 'copy-from-line-below': => InsertMode.copyCharacterFromBelow(@editor, this)
24c7594d
BB
78
79 @registerOperationCommands
80 'activate-insert-mode': => new Operators.Insert(@editor, this)
455f099b
BB
81 'activate-replace-mode': => new Operators.ReplaceMode(@editor, this)
82 'substitute': => [new Operators.Change(@editor, this), new Motions.MoveRight(@editor, this)]
83 'substitute-line': => [new Operators.Change(@editor, this), new Motions.MoveToRelativeLine(@editor, this)]
24c7594d
BB
84 'insert-after': => new Operators.InsertAfter(@editor, this)
85 'insert-after-end-of-line': => new Operators.InsertAfterEndOfLine(@editor, this)
86 'insert-at-beginning-of-line': => new Operators.InsertAtBeginningOfLine(@editor, this)
87 'insert-above-with-newline': => new Operators.InsertAboveWithNewline(@editor, this)
88 'insert-below-with-newline': => new Operators.InsertBelowWithNewline(@editor, this)
89 'delete': => @linewiseAliasedOperator(Operators.Delete)
90 'change': => @linewiseAliasedOperator(Operators.Change)
91 'change-to-last-character-of-line': => [new Operators.Change(@editor, this), new Motions.MoveToLastCharacterOfLine(@editor, this)]
92 'delete-right': => [new Operators.Delete(@editor, this), new Motions.MoveRight(@editor, this)]
93 'delete-left': => [new Operators.Delete(@editor, this), new Motions.MoveLeft(@editor, this)]
94 'delete-to-last-character-of-line': => [new Operators.Delete(@editor, this), new Motions.MoveToLastCharacterOfLine(@editor, this)]
95 'toggle-case': => new Operators.ToggleCase(@editor, this)
96 'upper-case': => new Operators.UpperCase(@editor, this)
97 'lower-case': => new Operators.LowerCase(@editor, this)
98 'toggle-case-now': => new Operators.ToggleCase(@editor, this, complete: true)
99 'yank': => @linewiseAliasedOperator(Operators.Yank)
100 'yank-line': => [new Operators.Yank(@editor, this), new Motions.MoveToRelativeLine(@editor, this)]
101 'put-before': => new Operators.Put(@editor, this, location: 'before')
102 'put-after': => new Operators.Put(@editor, this, location: 'after')
103 'join': => new Operators.Join(@editor, this)
104 'indent': => @linewiseAliasedOperator(Operators.Indent)
105 'outdent': => @linewiseAliasedOperator(Operators.Outdent)
106 'auto-indent': => @linewiseAliasedOperator(Operators.Autoindent)
107 'increase': => new Operators.Increase(@editor, this)
108 'decrease': => new Operators.Decrease(@editor, this)
109 'move-left': => new Motions.MoveLeft(@editor, this)
110 'move-up': => new Motions.MoveUp(@editor, this)
111 'move-down': => new Motions.MoveDown(@editor, this)
112 'move-right': => new Motions.MoveRight(@editor, this)
113 'move-to-next-word': => new Motions.MoveToNextWord(@editor, this)
114 'move-to-next-whole-word': => new Motions.MoveToNextWholeWord(@editor, this)
115 'move-to-end-of-word': => new Motions.MoveToEndOfWord(@editor, this)
116 'move-to-end-of-whole-word': => new Motions.MoveToEndOfWholeWord(@editor, this)
117 'move-to-previous-word': => new Motions.MoveToPreviousWord(@editor, this)
118 'move-to-previous-whole-word': => new Motions.MoveToPreviousWholeWord(@editor, this)
119 'move-to-next-paragraph': => new Motions.MoveToNextParagraph(@editor, this)
120 'move-to-previous-paragraph': => new Motions.MoveToPreviousParagraph(@editor, this)
121 'move-to-first-character-of-line': => new Motions.MoveToFirstCharacterOfLine(@editor, this)
122 'move-to-first-character-of-line-and-down': => new Motions.MoveToFirstCharacterOfLineAndDown(@editor, this)
123 'move-to-last-character-of-line': => new Motions.MoveToLastCharacterOfLine(@editor, this)
124 'move-to-last-nonblank-character-of-line-and-down': => new Motions.MoveToLastNonblankCharacterOfLineAndDown(@editor, this)
125 'move-to-beginning-of-line': (e) => @moveOrRepeat(e)
126 'move-to-first-character-of-line-up': => new Motions.MoveToFirstCharacterOfLineUp(@editor, this)
127 'move-to-first-character-of-line-down': => new Motions.MoveToFirstCharacterOfLineDown(@editor, this)
128 'move-to-start-of-file': => new Motions.MoveToStartOfFile(@editor, this)
129 'move-to-line': => new Motions.MoveToAbsoluteLine(@editor, this)
130 'move-to-top-of-screen': => new Motions.MoveToTopOfScreen(@editorElement, this)
131 'move-to-bottom-of-screen': => new Motions.MoveToBottomOfScreen(@editorElement, this)
132 'move-to-middle-of-screen': => new Motions.MoveToMiddleOfScreen(@editorElement, this)
133 'scroll-down': => new Scroll.ScrollDown(@editorElement)
134 'scroll-up': => new Scroll.ScrollUp(@editorElement)
135 'scroll-cursor-to-top': => new Scroll.ScrollCursorToTop(@editorElement)
136 'scroll-cursor-to-top-leave': => new Scroll.ScrollCursorToTop(@editorElement, {leaveCursor: true})
137 'scroll-cursor-to-middle': => new Scroll.ScrollCursorToMiddle(@editorElement)
138 'scroll-cursor-to-middle-leave': => new Scroll.ScrollCursorToMiddle(@editorElement, {leaveCursor: true})
139 'scroll-cursor-to-bottom': => new Scroll.ScrollCursorToBottom(@editorElement)
140 'scroll-cursor-to-bottom-leave': => new Scroll.ScrollCursorToBottom(@editorElement, {leaveCursor: true})
141 'scroll-half-screen-up': => new Motions.ScrollHalfUpKeepCursor(@editorElement, this)
142 'scroll-full-screen-up': => new Motions.ScrollFullUpKeepCursor(@editorElement, this)
143 'scroll-half-screen-down': => new Motions.ScrollHalfDownKeepCursor(@editorElement, this)
144 'scroll-full-screen-down': => new Motions.ScrollFullDownKeepCursor(@editorElement, this)
455f099b
BB
145 'scroll-cursor-to-left': => new Scroll.ScrollCursorToLeft(@editorElement)
146 'scroll-cursor-to-right': => new Scroll.ScrollCursorToRight(@editorElement)
24c7594d 147 'select-inside-word': => new TextObjects.SelectInsideWord(@editor)
455f099b 148 'select-inside-whole-word': => new TextObjects.SelectInsideWholeWord(@editor)
24c7594d
BB
149 'select-inside-double-quotes': => new TextObjects.SelectInsideQuotes(@editor, '"', false)
150 'select-inside-single-quotes': => new TextObjects.SelectInsideQuotes(@editor, '\'', false)
151 'select-inside-back-ticks': => new TextObjects.SelectInsideQuotes(@editor, '`', false)
152 'select-inside-curly-brackets': => new TextObjects.SelectInsideBrackets(@editor, '{', '}', false)
153 'select-inside-angle-brackets': => new TextObjects.SelectInsideBrackets(@editor, '<', '>', false)
154 'select-inside-tags': => new TextObjects.SelectInsideBrackets(@editor, '>', '<', false)
155 'select-inside-square-brackets': => new TextObjects.SelectInsideBrackets(@editor, '[', ']', false)
156 'select-inside-parentheses': => new TextObjects.SelectInsideBrackets(@editor, '(', ')', false)
157 'select-inside-paragraph': => new TextObjects.SelectInsideParagraph(@editor, false)
158 'select-a-word': => new TextObjects.SelectAWord(@editor)
455f099b 159 'select-a-whole-word': => new TextObjects.SelectAWholeWord(@editor)
24c7594d
BB
160 'select-around-double-quotes': => new TextObjects.SelectInsideQuotes(@editor, '"', true)
161 'select-around-single-quotes': => new TextObjects.SelectInsideQuotes(@editor, '\'', true)
162 'select-around-back-ticks': => new TextObjects.SelectInsideQuotes(@editor, '`', true)
163 'select-around-curly-brackets': => new TextObjects.SelectInsideBrackets(@editor, '{', '}', true)
164 'select-around-angle-brackets': => new TextObjects.SelectInsideBrackets(@editor, '<', '>', true)
165 'select-around-square-brackets': => new TextObjects.SelectInsideBrackets(@editor, '[', ']', true)
166 'select-around-parentheses': => new TextObjects.SelectInsideBrackets(@editor, '(', ')', true)
167 'select-around-paragraph': => new TextObjects.SelectAParagraph(@editor, true)
168 'register-prefix': (e) => @registerPrefix(e)
169 'repeat': (e) => new Operators.Repeat(@editor, this)
170 'repeat-search': (e) => new Motions.RepeatSearch(@editor, this)
171 'repeat-search-backwards': (e) => new Motions.RepeatSearch(@editor, this).reversed()
172 'move-to-mark': (e) => new Motions.MoveToMark(@editor, this)
173 'move-to-mark-literal': (e) => new Motions.MoveToMark(@editor, this, false)
174 'mark': (e) => new Operators.Mark(@editor, this)
175 'find': (e) => new Motions.Find(@editor, this)
176 'find-backwards': (e) => new Motions.Find(@editor, this).reverse()
177 'till': (e) => new Motions.Till(@editor, this)
178 'till-backwards': (e) => new Motions.Till(@editor, this).reverse()
455f099b
BB
179 'repeat-find': (e) => new @globalVimState.currentFind.constructor(@editor, this, repeated: true) if @globalVimState.currentFind
180 'repeat-find-reverse': (e) => new @globalVimState.currentFind.constructor(@editor, this, repeated: true, reverse: true) if @globalVimState.currentFind
24c7594d
BB
181 'replace': (e) => new Operators.Replace(@editor, this)
182 'search': (e) => new Motions.Search(@editor, this)
183 'reverse-search': (e) => (new Motions.Search(@editor, this)).reversed()
184 'search-current-word': (e) => new Motions.SearchCurrentWord(@editor, this)
185 'bracket-matching-motion': (e) => new Motions.BracketMatchingMotion(@editor, this)
186 'reverse-search-current-word': (e) => (new Motions.SearchCurrentWord(@editor, this)).reversed()
187
188 # Private: Register multiple command handlers via an {Object} that maps
189 # command names to command handler functions.
190 #
191 # Prefixes the given command names with 'vim-mode:' to reduce redundancy in
192 # the provided object.
193 registerCommands: (commands) ->
194 for commandName, fn of commands
195 do (fn) =>
196 @subscriptions.add(atom.commands.add(@editorElement, "vim-mode:#{commandName}", fn))
197
198 # Private: Register multiple Operators via an {Object} that
199 # maps command names to functions that return operations to push.
200 #
201 # Prefixes the given command names with 'vim-mode:' to reduce redundancy in
202 # the given object.
203 registerOperationCommands: (operationCommands) ->
204 commands = {}
205 for commandName, operationFn of operationCommands
206 do (operationFn) =>
207 commands[commandName] = (event) => @pushOperations(operationFn(event))
208 @registerCommands(commands)
209
210 # Private: Push the given operations onto the operation stack, then process
211 # it.
212 pushOperations: (operations) ->
213 return unless operations?
214 operations = [operations] unless _.isArray(operations)
215
216 for operation in operations
217 # Motions in visual mode perform their selections.
218 if @mode is 'visual' and (operation instanceof Motions.Motion or operation instanceof TextObjects.TextObject)
219 operation.execute = operation.select
220
221 # if we have started an operation that responds to canComposeWith check if it can compose
222 # with the operation we're going to push onto the stack
223 if (topOp = @topOperation())? and topOp.canComposeWith? and not topOp.canComposeWith(operation)
455f099b 224 @resetNormalMode()
24c7594d
BB
225 @emitter.emit('failed-to-compose')
226 break
227
228 @opStack.push(operation)
229
230 # If we've received an operator in visual mode, mark the current
231 # selection as the motion to operate on.
232 if @mode is 'visual' and operation instanceof Operators.Operator
233 @opStack.push(new Motions.CurrentSelection(@editor, this))
234
235 @processOpStack()
236
237 onDidFailToCompose: (fn) ->
238 @emitter.on('failed-to-compose', fn)
239
240 onDidDestroy: (fn) ->
241 @emitter.on('did-destroy', fn)
242
243 # Private: Removes all operations from the stack.
244 #
245 # Returns nothing.
246 clearOpStack: ->
247 @opStack = []
248
249 undo: ->
250 @editor.undo()
455f099b 251 @activateNormalMode()
24c7594d
BB
252
253 # Private: Processes the command if the last operation is complete.
254 #
255 # Returns nothing.
256 processOpStack: ->
257 unless @opStack.length > 0
258 return
259
260 unless @topOperation().isComplete()
455f099b 261 if @mode is 'normal' and @topOperation() instanceof Operators.Operator
24c7594d
BB
262 @activateOperatorPendingMode()
263 return
264
265 poppedOperation = @opStack.pop()
266 if @opStack.length
267 try
268 @topOperation().compose(poppedOperation)
269 @processOpStack()
270 catch e
271 if (e instanceof Operators.OperatorError) or (e instanceof Motions.MotionError)
455f099b 272 @resetNormalMode()
24c7594d
BB
273 else
274 throw e
275 else
276 @history.unshift(poppedOperation) if poppedOperation.isRecordable()
277 poppedOperation.execute()
278
279 # Private: Fetches the last operation.
280 #
281 # Returns the last operation.
282 topOperation: ->
283 _.last @opStack
284
285 # Private: Fetches the value of a given register.
286 #
287 # name - The name of the register to fetch.
288 #
289 # Returns the value of the given register or undefined if it hasn't
290 # been set.
291 getRegister: (name) ->
455f099b
BB
292 if name is '"'
293 name = settings.defaultRegister()
24c7594d
BB
294 if name in ['*', '+']
295 text = atom.clipboard.read()
296 type = Utils.copyType(text)
297 {text, type}
298 else if name is '%'
299 text = @editor.getURI()
300 type = Utils.copyType(text)
301 {text, type}
302 else if name is "_" # Blackhole always returns nothing
303 text = ''
304 type = Utils.copyType(text)
305 {text, type}
306 else
307 @globalVimState.registers[name.toLowerCase()]
308
309 # Private: Fetches the value of a given mark.
310 #
311 # name - The name of the mark to fetch.
312 #
313 # Returns the value of the given mark or undefined if it hasn't
314 # been set.
315 getMark: (name) ->
316 if @marks[name]
317 @marks[name].getBufferRange().start
318 else
319 undefined
320
321 # Private: Sets the value of a given register.
322 #
323 # name - The name of the register to fetch.
324 # value - The value to set the register to.
325 #
326 # Returns nothing.
327 setRegister: (name, value) ->
455f099b
BB
328 if name is '"'
329 name = settings.defaultRegister()
24c7594d
BB
330 if name in ['*', '+']
331 atom.clipboard.write(value.text)
332 else if name is '_'
333 # Blackhole register, nothing to do
334 else if /^[A-Z]$/.test(name)
335 @appendRegister(name.toLowerCase(), value)
336 else
337 @globalVimState.registers[name] = value
338
339
340 # Private: append a value into a given register
341 # like setRegister, but appends the value
342 appendRegister: (name, value) ->
343 register = @globalVimState.registers[name] ?=
344 type: 'character'
345 text: ""
346 if register.type is 'linewise' and value.type isnt 'linewise'
347 register.text += value.text + '\n'
348 else if register.type isnt 'linewise' and value.type is 'linewise'
349 register.text += '\n' + value.text
350 register.type = 'linewise'
351 else
352 register.text += value.text
353
354 # Private: Sets the value of a given mark.
355 #
356 # name - The name of the mark to fetch.
357 # pos {Point} - The value to set the mark to.
358 #
359 # Returns nothing.
360 setMark: (name, pos) ->
361 # check to make sure name is in [a-z] or is `
362 if (charCode = name.charCodeAt(0)) >= 96 and charCode <= 122
363 marker = @editor.markBufferRange(new Range(pos, pos), {invalidate: 'never', persistent: false})
364 @marks[name] = marker
365
366 # Public: Append a search to the search history.
367 #
368 # Motions.Search - The confirmed search motion to append
369 #
370 # Returns nothing
371 pushSearchHistory: (search) ->
372 @globalVimState.searchHistory.unshift search
373
374 # Public: Get the search history item at the given index.
375 #
376 # index - the index of the search history item
377 #
378 # Returns a search motion
379 getSearchHistoryItem: (index = 0) ->
380 @globalVimState.searchHistory[index]
381
382 ##############################################################################
383 # Mode Switching
384 ##############################################################################
385
455f099b 386 # Private: Used to enable normal mode.
24c7594d
BB
387 #
388 # Returns nothing.
455f099b 389 activateNormalMode: ->
24c7594d
BB
390 @deactivateInsertMode()
391 @deactivateVisualMode()
392
455f099b 393 @mode = 'normal'
24c7594d
BB
394 @submode = null
395
455f099b 396 @changeModeClass('normal-mode')
24c7594d
BB
397
398 @clearOpStack()
399 selection.clear(autoscroll: false) for selection in @editor.getSelections()
455f099b 400 @ensureCursorsWithinLine()
24c7594d
BB
401
402 @updateStatusBar()
403
455f099b
BB
404 # TODO: remove this method and bump the `vim-mode` service version number.
405 activateCommandMode: ->
406 Grim.deprecate("Use ::activateNormalMode instead")
407 @activateNormalMode()
408
24c7594d
BB
409 # Private: Used to enable insert mode.
410 #
411 # Returns nothing.
455f099b 412 activateInsertMode: (subtype = null) ->
24c7594d
BB
413 @mode = 'insert'
414 @editorElement.component.setInputEnabled(true)
415 @setInsertionCheckpoint()
455f099b 416 @submode = subtype
24c7594d
BB
417 @changeModeClass('insert-mode')
418 @updateStatusBar()
419
455f099b
BB
420 activateReplaceMode: ->
421 @activateInsertMode('replace')
422 @replaceModeCounter = 0
423 @editorElement.classList.add('replace-mode')
424 @subscriptions.add @replaceModeListener = @editor.onWillInsertText @replaceModeInsertHandler
425 @subscriptions.add @replaceModeUndoListener = @editor.onDidInsertText @replaceModeUndoHandler
426
427 replaceModeInsertHandler: (event) =>
428 chars = event.text?.split('') or []
429 selections = @editor.getSelections()
430 for char in chars
431 continue if char is '\n'
432 for selection in selections
433 selection.delete() unless selection.cursor.isAtEndOfLine()
434 return
435
436 replaceModeUndoHandler: (event) =>
437 @replaceModeCounter++
438
439 replaceModeUndo: ->
440 if @replaceModeCounter > 0
441 @editor.undo()
442 @editor.undo()
443 @editor.moveLeft()
444 @replaceModeCounter--
445
24c7594d
BB
446 setInsertionCheckpoint: ->
447 @insertionCheckpoint = @editor.createCheckpoint() unless @insertionCheckpoint?
448
449 deactivateInsertMode: ->
450 return unless @mode in [null, 'insert']
451 @editorElement.component.setInputEnabled(false)
455f099b 452 @editorElement.classList.remove('replace-mode')
24c7594d
BB
453 @editor.groupChangesSinceCheckpoint(@insertionCheckpoint)
454 changes = getChangesSinceCheckpoint(@editor.buffer, @insertionCheckpoint)
455 item = @inputOperator(@history[0])
456 @insertionCheckpoint = null
457 if item?
458 item.confirmChanges(changes)
459 for cursor in @editor.getCursors()
460 cursor.moveLeft() unless cursor.isAtBeginningOfLine()
455f099b
BB
461 if @replaceModeListener?
462 @replaceModeListener.dispose()
463 @subscriptions.remove @replaceModeListener
464 @replaceModeListener = null
465 @replaceModeUndoListener.dispose()
466 @subscriptions.remove @replaceModeUndoListener
467 @replaceModeUndoListener = null
24c7594d
BB
468
469 deactivateVisualMode: ->
470 return unless @mode is 'visual'
471 for selection in @editor.getSelections()
472 selection.cursor.moveLeft() unless (selection.isEmpty() or selection.isReversed())
473
474 # Private: Get the input operator that needs to be told about about the
475 # typed undo transaction in a recently completed operation, if there
476 # is one.
477 inputOperator: (item) ->
478 return item unless item?
479 return item if item.inputOperator?()
480 return item.composedObject if item.composedObject?.inputOperator?()
481
482 # Private: Used to enable visual mode.
483 #
484 # type - One of 'characterwise', 'linewise' or 'blockwise'
485 #
486 # Returns nothing.
487 activateVisualMode: (type) ->
488 # Already in 'visual', this means one of following command is
489 # executed within `vim-mode.visual-mode`
490 # * activate-blockwise-visual-mode
491 # * activate-characterwise-visual-mode
492 # * activate-linewise-visual-mode
493 if @mode is 'visual'
494 if @submode is type
455f099b 495 @activateNormalMode()
24c7594d
BB
496 return
497
498 @submode = type
499 if @submode is 'linewise'
500 for selection in @editor.getSelections()
501 # Keep original range as marker's property to get back
502 # to characterwise.
503 # Since selectLine lost original cursor column.
504 originalRange = selection.getBufferRange()
505 selection.marker.setProperties({originalRange})
506 [start, end] = selection.getBufferRowRange()
507 selection.selectLine(row) for row in [start..end]
508
509 else if @submode in ['characterwise', 'blockwise']
510 # Currently, 'blockwise' is not yet implemented.
511 # So treat it as characterwise.
512 # Recover original range.
513 for selection in @editor.getSelections()
514 {originalRange} = selection.marker.getProperties()
515 if originalRange
516 [startRow, endRow] = selection.getBufferRowRange()
517 originalRange.start.row = startRow
518 originalRange.end.row = endRow
519 selection.setBufferRange(originalRange)
520 else
521 @deactivateInsertMode()
522 @mode = 'visual'
523 @submode = type
524 @changeModeClass('visual-mode')
525
526 if @submode is 'linewise'
527 @editor.selectLinesContainingCursors()
528 else if @editor.getSelectedText() is ''
529 @editor.selectRight()
530
531 @updateStatusBar()
532
533 # Private: Used to re-enable visual mode
534 resetVisualMode: ->
535 @activateVisualMode(@submode)
536
537 # Private: Used to enable operator-pending mode.
538 activateOperatorPendingMode: ->
539 @deactivateInsertMode()
540 @mode = 'operator-pending'
455f099b 541 @submode = null
24c7594d
BB
542 @changeModeClass('operator-pending-mode')
543
544 @updateStatusBar()
545
546 changeModeClass: (targetMode) ->
455f099b 547 for mode in ['normal-mode', 'insert-mode', 'visual-mode', 'operator-pending-mode']
24c7594d
BB
548 if mode is targetMode
549 @editorElement.classList.add(mode)
550 else
551 @editorElement.classList.remove(mode)
552
455f099b 553 # Private: Resets the normal mode back to it's initial state.
24c7594d
BB
554 #
555 # Returns nothing.
455f099b 556 resetNormalMode: ->
24c7594d
BB
557 @clearOpStack()
558 @editor.clearSelections()
455f099b 559 @activateNormalMode()
24c7594d
BB
560
561 # Private: A generic way to create a Register prefix based on the event.
562 #
563 # e - The event that triggered the Register prefix.
564 #
565 # Returns nothing.
566 registerPrefix: (e) ->
455f099b
BB
567 new Prefixes.Register(@registerName(e))
568
569 # Private: Gets a register name from a keyboard event
570 #
571 # e - The event
572 #
573 # Returns the name of the register
574 registerName: (e) ->
24c7594d
BB
575 keyboardEvent = e.originalEvent?.originalEvent ? e.originalEvent
576 name = atom.keymaps.keystrokeForKeyboardEvent(keyboardEvent)
577 if name.lastIndexOf('shift-', 0) is 0
578 name = name.slice(6)
455f099b 579 name
24c7594d
BB
580
581 # Private: A generic way to create a Number prefix based on the event.
582 #
583 # e - The event that triggered the Number prefix.
584 #
585 # Returns nothing.
586 repeatPrefix: (e) ->
587 keyboardEvent = e.originalEvent?.originalEvent ? e.originalEvent
588 num = parseInt(atom.keymaps.keystrokeForKeyboardEvent(keyboardEvent))
589 if @topOperation() instanceof Prefixes.Repeat
590 @topOperation().addDigit(num)
591 else
592 if num is 0
593 e.abortKeyBinding()
594 else
595 @pushOperations(new Prefixes.Repeat(num))
596
597 reverseSelections: ->
455f099b 598 reversed = not @editor.getLastSelection().isReversed()
24c7594d 599 for selection in @editor.getSelections()
24c7594d
BB
600 selection.setBufferRange(selection.getBufferRange(), {reversed})
601
602 # Private: Figure out whether or not we are in a repeat sequence or we just
603 # want to move to the beginning of the line. If we are within a repeat
604 # sequence, we pass control over to @repeatPrefix.
605 #
606 # e - The triggered event.
607 #
608 # Returns new motion or nothing.
609 moveOrRepeat: (e) ->
610 if @topOperation() instanceof Prefixes.Repeat
611 @repeatPrefix(e)
612 null
613 else
614 new Motions.MoveToBeginningOfLine(@editor, this)
615
616 # Private: A generic way to handle Operators that can be repeated for
617 # their linewise form.
618 #
619 # constructor - The constructor of the operator.
620 #
621 # Returns nothing.
622 linewiseAliasedOperator: (constructor) ->
623 if @isOperatorPending(constructor)
624 new Motions.MoveToRelativeLine(@editor, this)
625 else
626 new constructor(@editor, this)
627
628 # Private: Check if there is a pending operation of a certain type, or
629 # if there is any pending operation, if no type given.
630 #
631 # constructor - The constructor of the object type you're looking for.
632 #
633 isOperatorPending: (constructor) ->
634 if constructor?
635 for op in @opStack
636 return op if op instanceof constructor
637 false
638 else
639 @opStack.length > 0
640
641 updateStatusBar: ->
642 @statusBarManager.update(@mode, @submode)
643
455f099b
BB
644 # Private: insert the contents of the register in the editor
645 #
646 # name - the name of the register to insert
647 #
648 # Returns nothing.
649 insertRegister: (name) ->
650 text = @getRegister(name)?.text
651 @editor.insertText(text) if text?
652
653 # Private: ensure the mode follows the state of selections
654 checkSelections: =>
655 return unless @editor?
656 if @editor.getSelections().every((selection) -> selection.isEmpty())
657 @ensureCursorsWithinLine() if @mode is 'normal'
658 @activateNormalMode() if @mode is 'visual'
659 else
660 @activateVisualMode('characterwise') if @mode is 'normal'
661
662 # Private: ensure the cursor stays within the line as appropriate
663 ensureCursorsWithinLine: =>
664 for cursor in @editor.getCursors()
665 {goalColumn} = cursor
666 if cursor.isAtEndOfLine() and not cursor.isAtBeginningOfLine()
667 cursor.moveLeft()
668 cursor.goalColumn = goalColumn
669
670 @editor.mergeCursors()
671
24c7594d
BB
672# This uses private APIs and may break if TextBuffer is refactored.
673# Package authors - copy and paste this code at your own risk.
674getChangesSinceCheckpoint = (buffer, checkpoint) ->
675 {history} = buffer
676
677 if (index = history.getCheckpointIndex(checkpoint))?
678 history.undoStack.slice(index)
679 else
680 []