laughing-hipster/.atom/packages/vim-mode/lib/vim-state.coffee

521 lines
19 KiB
CoffeeScript

_ = require 'underscore-plus'
{$} = require 'atom'
Operators = require './operators/index'
Prefixes = require './prefixes'
Motions = require './motions/index'
TextObjects = require './text-objects'
Utils = require './utils'
Panes = require './panes'
Scroll = require './scroll'
{$$, Point, Range} = require 'atom'
Marker = require 'atom'
module.exports =
class VimState
editor: null
opStack: null
mode: null
submode: null
constructor: (@editorView) ->
@editor = @editorView.editor
@opStack = []
@history = []
@marks = {}
@desiredCursorColumn = null
params = {}
params.manager = this;
params.id = 0;
@setupCommandMode()
@editorView.setInputEnabled?(false)
@registerInsertIntercept()
@registerInsertTransactionResets()
@registerUndoIntercept()
if atom.config.get 'vim-mode.startInInsertMode'
@activateInsertMode()
else
@activateCommandMode()
atom.project.eachBuffer (buffer) =>
@registerChangeHandler(buffer)
# Private: Creates a handle to block insertion while in command mode.
#
# This is currently a bit of a hack. If a user is in command mode they
# won't be able to type in any of Atom's dialogs (such as the command
# palette). This also doesn't block non-printable characters such as
# backspace.
#
# There should probably be a better API on the editor to handle this
# but the requirements aren't clear yet, so this will have to suffice
# for now.
#
# Returns nothing.
registerInsertIntercept: ->
@editorView.preempt 'textInput', (e) =>
return if $(e.currentTarget).hasClass('mini')
if @mode == 'insert'
true
else
@clearOpStack()
false
# Private: Intercept undo in insert mode.
#
# Undo in insert mode will blow up the previous transaction, but not
# put it into the redo stack anywhere correctly, as it hasn't been
# completed. As a workaround, we exit insert mode first and then
# bubble the event up
registerUndoIntercept: ->
@editorView.preempt 'core:undo', (e) =>
return true unless @mode == 'insert'
@activateCommandMode()
return true
# Private: Reset transactions on input for undo/redo/repeat on several
# core and vim-mode events
registerInsertTransactionResets: ->
events = [ 'core:move-up'
'core:move-down'
'core:move-right'
'core:move-left' ]
@editorView.on events.join(' '), =>
@resetInputTransactions()
# Private: Watches for any deletes on the current buffer and places it in the
# last deleted buffer.
#
# Returns nothing.
registerChangeHandler: (buffer) ->
buffer.on 'changed', ({newRange, newText, oldRange, oldText}) =>
return unless @setRegister?
if newText == ''
@setRegister('"', text: oldText, type: Utils.copyType(oldText))
# Private: Creates the plugin's bindings
#
# Returns nothing.
setupCommandMode: ->
@registerCommands
'activate-command-mode': => @activateCommandMode()
'activate-linewise-visual-mode': => @activateVisualMode('linewise')
'activate-characterwise-visual-mode': => @activateVisualMode('characterwise')
'activate-blockwise-visual-mode': => @activateVisualMode('blockwise')
'reset-command-mode': => @resetCommandMode()
'repeat-prefix': (e) => @repeatPrefix(e)
@registerOperationCommands
'activate-insert-mode': => new Operators.Insert(@editor, @)
'substitute': => new Operators.Substitute(@editor, @)
'substitute-line': => new Operators.SubstituteLine(@editor, @)
'insert-after': => new Operators.InsertAfter(@editor, @)
'insert-after-end-of-line': => [new Motions.MoveToLastCharacterOfLine(@editor, @), new Operators.InsertAfter(@editor, @)]
'insert-at-beginning-of-line': => [new Motions.MoveToFirstCharacterOfLine(@editor, @), new Operators.Insert(@editor, @)]
'insert-above-with-newline': => new Operators.InsertAboveWithNewline(@editor, @)
'insert-below-with-newline': => new Operators.InsertBelowWithNewline(@editor, @)
'delete': => @linewiseAliasedOperator(Operators.Delete)
'change': => @linewiseAliasedOperator(Operators.Change)
'change-to-last-character-of-line': => [new Operators.Change(@editor, @), new Motions.MoveToLastCharacterOfLine(@editor, @)]
'delete-right': => [new Operators.Delete(@editor, @), new Motions.MoveRight(@editor, @)]
'delete-left': => [new Operators.Delete(@editor, @), new Motions.MoveLeft(@editor, @)]
'delete-to-last-character-of-line': => [new Operators.Delete(@editor, @), new Motions.MoveToLastCharacterOfLine(@editor, @)]
'toggle-case': => new Operators.ToggleCase(@editor, @)
'yank': => @linewiseAliasedOperator(Operators.Yank)
'yank-line': => [new Operators.Yank(@editor, @), new Motions.MoveToLine(@editor, @)]
'put-before': => new Operators.Put(@editor, @, location: 'before')
'put-after': => new Operators.Put(@editor, @, location: 'after')
'join': => new Operators.Join(@editor, @)
'indent': => @linewiseAliasedOperator(Operators.Indent)
'outdent': => @linewiseAliasedOperator(Operators.Outdent)
'auto-indent': => @linewiseAliasedOperator(Operators.Autoindent)
'move-left': => new Motions.MoveLeft(@editor, @)
'move-up': => new Motions.MoveUp(@editor, @)
'move-down': => new Motions.MoveDown(@editor, @)
'move-right': => new Motions.MoveRight(@editor, @)
'move-to-next-word': => new Motions.MoveToNextWord(@editor, @)
'move-to-next-whole-word': => new Motions.MoveToNextWholeWord(@editor, @)
'move-to-end-of-word': => new Motions.MoveToEndOfWord(@editor, @)
'move-to-end-of-whole-word': => new Motions.MoveToEndOfWholeWord(@editor, @)
'move-to-previous-word': => new Motions.MoveToPreviousWord(@editor, @)
'move-to-previous-whole-word': => new Motions.MoveToPreviousWholeWord(@editor, @)
'move-to-next-paragraph': => new Motions.MoveToNextParagraph(@editor, @)
'move-to-previous-paragraph': => new Motions.MoveToPreviousParagraph(@editor, @)
'move-to-first-character-of-line': => new Motions.MoveToFirstCharacterOfLine(@editor, @)
'move-to-last-character-of-line': => new Motions.MoveToLastCharacterOfLine(@editor, @)
'move-to-beginning-of-line': (e) => @moveOrRepeat(e)
'move-to-first-character-of-line-up': => new Motions.MoveToFirstCharacterOfLineUp(@editor, @)
'move-to-first-character-of-line-down': => new Motions.MoveToFirstCharacterOfLineDown(@editor, @)
'move-to-start-of-file': => new Motions.MoveToStartOfFile(@editor, @)
'move-to-line': => new Motions.MoveToLine(@editor, @)
'move-to-top-of-screen': => new Motions.MoveToTopOfScreen(@editor, @, @editorView)
'move-to-bottom-of-screen': => new Motions.MoveToBottomOfScreen(@editor, @, @editorView)
'move-to-middle-of-screen': => new Motions.MoveToMiddleOfScreen(@editor, @, @editorView)
'scroll-down': => new Scroll.ScrollDown(@editorView, @editor)
'scroll-up': => new Scroll.ScrollUp(@editorView, @editor)
'select-inside-word': => new TextObjects.SelectInsideWord(@editor)
'select-inside-double-quotes': => new TextObjects.SelectInsideQuotes(@editor, '"')
'select-inside-single-quotes': => new TextObjects.SelectInsideQuotes(@editor, '\'')
'select-inside-curly-brackets': => new TextObjects.SelectInsideBrackets(@editor, '{', '}')
'select-inside-angle-brackets': => new TextObjects.SelectInsideBrackets(@editor, '<', '>')
'select-inside-parentheses': => new TextObjects.SelectInsideBrackets(@editor, '(', ')')
'register-prefix': (e) => @registerPrefix(e)
'repeat': (e) => new Operators.Repeat(@editor, @)
'repeat-search': (e) => currentSearch.repeat() if (currentSearch = Motions.Search.currentSearch)?
'repeat-search-backwards': (e) => currentSearch.repeat(backwards: true) if (currentSearch = Motions.Search.currentSearch)?
'focus-pane-view-on-left': => new Panes.FocusPaneViewOnLeft()
'focus-pane-view-on-right': => new Panes.FocusPaneViewOnRight()
'focus-pane-view-above': => new Panes.FocusPaneViewAbove()
'focus-pane-view-below': => new Panes.FocusPaneViewBelow()
'focus-previous-pane-view': => new Panes.FocusPreviousPaneView()
'move-to-mark': (e) => new Motions.MoveToMark(@editorView, @)
'move-to-mark-literal': (e) => new Motions.MoveToMark(@editorView, @, false)
'mark': (e) => new Operators.Mark(@editorView, @)
'find': (e) => new Motions.Find(@editorView, @)
'find-backwards': (e) => new Motions.Find(@editorView, @).reverse()
'till': (e) => new Motions.Till(@editorView, @)
'till-backwards': (e) => new Motions.Till(@editorView, @).reverse()
'replace': (e) => new Operators.Replace(@editorView, @)
'search': (e) => new Motions.Search(@editorView, @)
'reverse-search': (e) => (new Motions.Search(@editorView, @)).reversed()
'search-current-word': (e) => new Motions.SearchCurrentWord(@editorView, @)
'bracket-matching-motion': (e) => new Motions.BracketMatchingMotion(@editorView,@)
'reverse-search-current-word': (e) => (new Motions.SearchCurrentWord(@editorView, @)).reversed()
# Private: Register multiple command handlers via an {Object} that maps
# command names to command handler functions.
#
# Prefixes the given command names with 'vim-mode:' to reduce redundancy in
# the provided object.
registerCommands: (commands) ->
for commandName, fn of commands
do (fn) =>
@editorView.command "vim-mode:#{commandName}.vim-mode", fn
# Private: Register multiple Operators via an {Object} that
# maps command names to functions that return operations to push.
#
# Prefixes the given command names with 'vim-mode:' to reduce redundancy in
# the given object.
registerOperationCommands: (operationCommands) ->
commands = {}
for commandName, operationFn of operationCommands
do (operationFn) =>
commands[commandName] = (event) => @pushOperations(operationFn(event))
@registerCommands(commands)
# Private: Push the given operations onto the operation stack, then process
# it.
pushOperations: (operations) ->
return unless operations?
operations = [operations] unless _.isArray(operations)
for operation in operations
# Motions in visual mode perform their selections.
if @mode is 'visual' and (operation instanceof Motions.Motion or operation instanceof TextObjects.TextObject)
operation.execute = operation.select
# if we have started an operation that responds to canComposeWith check if it can compose
# with the operation we're going to push onto the stack
if (topOp = @topOperation())? and topOp.canComposeWith? and not topOp.canComposeWith(operation)
@editorView.trigger 'vim-mode:compose-failure'
@resetCommandMode()
break
@opStack.push(operation)
# If we've received an operator in visual mode, mark the current
# selection as the motion to operate on.
if @mode is 'visual' and operation instanceof Operators.Operator
@opStack.push(new Motions.CurrentSelection(@editor, @))
@processOpStack()
# Private: Removes all operations from the stack.
#
# Returns nothing.
clearOpStack: ->
@opStack = []
# Private: Processes the command if the last operation is complete.
#
# Returns nothing.
processOpStack: ->
unless @opStack.length > 0
return
unless @topOperation().isComplete()
if @mode is 'command' and @topOperation() instanceof Operators.Operator
@activateOperatorPendingMode()
return
poppedOperation = @opStack.pop()
if @opStack.length
try
@topOperation().compose(poppedOperation)
@processOpStack()
catch e
((e instanceof Operators.OperatorError) or (e instanceof Motions.MotionError)) and @resetCommandMode() or throw e
else
@history.unshift(poppedOperation) if poppedOperation.isRecordable()
poppedOperation.execute()
# Private: Fetches the last operation.
#
# Returns the last operation.
topOperation: ->
_.last @opStack
# Private: Fetches the value of a given register.
#
# name - The name of the register to fetch.
#
# Returns the value of the given register or undefined if it hasn't
# been set.
getRegister: (name) ->
if name in ['*', '+']
text = atom.clipboard.read()
type = Utils.copyType(text)
{text, type}
else if name == '%'
text = @editor.getUri()
type = Utils.copyType(text)
{text, type}
else if name == "_" # Blackhole always returns nothing
text = ''
type = Utils.copyType(text)
{text, type}
else
atom.workspace.vimState.registers[name]
# Private: Fetches the value of a given mark.
#
# name - The name of the mark to fetch.
#
# Returns the value of the given mark or undefined if it hasn't
# been set.
getMark: (name) ->
if @marks[name]
@marks[name].getBufferRange().start
else
undefined
# Private: Sets the value of a given register.
#
# name - The name of the register to fetch.
# value - The value to set the register to.
#
# Returns nothing.
setRegister: (name, value) ->
if name in ['*', '+']
atom.clipboard.write(value.text)
else if name == '_'
# Blackhole register, nothing to do
else
atom.workspace.vimState.registers[name] = value
# Private: Sets the value of a given mark.
#
# name - The name of the mark to fetch.
# pos {Point} - The value to set the mark to.
#
# Returns nothing.
setMark: (name, pos) ->
# check to make sure name is in [a-z] or is `
if (charCode = name.charCodeAt(0)) >= 96 and charCode <= 122
marker = @editor.markBufferRange(new Range(pos,pos),{invalidate:'never',persistent:false})
@marks[name] = marker
# Public: Append a search to the search history.
#
# Motions.Search - The confirmed search motion to append
#
# Returns nothing
pushSearchHistory: (search) ->
atom.workspace.vimState.searchHistory.unshift search
# Public: Get the search history item at the given index.
#
# index - the index of the search history item
#
# Returns a search motion
getSearchHistoryItem: (index) ->
atom.workspace.vimState.searchHistory[index]
resetInputTransactions: ->
return unless @mode == 'insert' && @history[0]?.inputOperator?()
@deactivateInsertMode()
@activateInsertMode()
##############################################################################
# Mode Switching
##############################################################################
# Private: Used to enable command mode.
#
# Returns nothing.
activateCommandMode: ->
@deactivateInsertMode()
@mode = 'command'
@submode = null
if @editorView.is(".insert-mode")
cursor = @editor.getCursor()
cursor.moveLeft() unless cursor.isAtBeginningOfLine()
@changeModeClass('command-mode')
@clearOpStack()
@editor.clearSelections()
@updateStatusBar()
# Private: Used to enable insert mode.
#
# Returns nothing.
activateInsertMode: (transactionStarted = false)->
@mode = 'insert'
@editorView.setInputEnabled?(true)
@editor.beginTransaction() unless transactionStarted
@submode = null
@changeModeClass('insert-mode')
@updateStatusBar()
deactivateInsertMode: ->
return unless @mode == 'insert'
@editorView.setInputEnabled?(false)
@editor.commitTransaction()
transaction = _.last(@editor.buffer.history.undoStack)
item = @inputOperator(@history[0])
if item? and transaction?
item.confirmTransaction(transaction)
# Private: Get the input operator that needs to be told about about the
# typed undo transaction in a recently completed operation, if there
# is one.
inputOperator: (item) ->
return item unless item?
return item if item.inputOperator?()
return item.composedObject if item.composedObject?.inputOperator?()
# Private: Used to enable visual mode.
#
# type - One of 'characterwise', 'linewise' or 'blockwise'
#
# Returns nothing.
activateVisualMode: (type) ->
@deactivateInsertMode()
@mode = 'visual'
@submode = type
@changeModeClass('visual-mode')
if @submode == 'linewise'
@editor.selectLine()
@updateStatusBar()
# Private: Used to enable operator-pending mode.
activateOperatorPendingMode: ->
@deactivateInsertMode()
@mode = 'operator-pending'
@submodule = null
@changeModeClass('operator-pending-mode')
@updateStatusBar()
changeModeClass: (targetMode) ->
for mode in ['command-mode', 'insert-mode', 'visual-mode', 'operator-pending-mode']
if mode is targetMode
@editorView.addClass(mode)
else
@editorView.removeClass(mode)
# Private: Resets the command mode back to it's initial state.
#
# Returns nothing.
resetCommandMode: ->
@activateCommandMode()
# Private: A generic way to create a Register prefix based on the event.
#
# e - The event that triggered the Register prefix.
#
# Returns nothing.
registerPrefix: (e) ->
name = atom.keymap.keystrokeStringForEvent(e.originalEvent)
new Prefixes.Register(name)
# Private: A generic way to create a Number prefix based on the event.
#
# e - The event that triggered the Number prefix.
#
# Returns nothing.
repeatPrefix: (e) ->
num = parseInt(atom.keymap.keystrokeStringForEvent(e.originalEvent))
if @topOperation() instanceof Prefixes.Repeat
@topOperation().addDigit(num)
else
if num is 0
e.abortKeyBinding()
else
@pushOperations(new Prefixes.Repeat(num))
# Private: Figure out whether or not we are in a repeat sequence or we just
# want to move to the beginning of the line. If we are within a repeat
# sequence, we pass control over to @repeatPrefix.
#
# e - The triggered event.
#
# Returns new motion or nothing.
moveOrRepeat: (e) ->
if @topOperation() instanceof Prefixes.Repeat
@repeatPrefix(e)
null
else
new Motions.MoveToBeginningOfLine(@editor, @)
# Private: A generic way to handle Operators that can be repeated for
# their linewise form.
#
# constructor - The constructor of the operator.
#
# Returns nothing.
linewiseAliasedOperator: (constructor) ->
if @isOperatorPending(constructor)
new Motions.MoveToLine(@editor, @)
else
new constructor(@editor, @)
# Private: Check if there is a pending operation of a certain type, or
# if there is any pending operation, if no type given.
#
# constructor - The constructor of the object type you're looking for.
#
isOperatorPending: (constructor) ->
if constructor?
for op in @opStack
return op if op instanceof constructor
false
else
@opStack.length > 0
updateStatusBar: ->
atom.packages.once 'activated', =>
if !$('#status-bar-vim-mode').length
atom.workspaceView.statusBar?.prependRight("<div id='status-bar-vim-mode' class='inline-block'>Command</div>")
@updateStatusBar()
@removeStatusBarClass()
switch @mode
when 'insert' then $('#status-bar-vim-mode').addClass('status-bar-vim-mode-insert').html("Insert")
when 'command' then $('#status-bar-vim-mode').addClass('status-bar-vim-mode-command').html("Command")
when 'visual' then $('#status-bar-vim-mode').addClass('status-bar-vim-mode-visual').html("Visual")
removeStatusBarClass: -> $('#status-bar-vim-mode').removeClass('status-bar-vim-mode-insert status-bar-vim-mode-command status-bar-vim-mode-visual')