diff --git a/lib/base.coffee b/lib/base.coffee
deleted file mode 100644
index 1e7aad70a..000000000
--- a/lib/base.coffee
+++ /dev/null
@@ -1,389 +0,0 @@
-# To avoid loading underscore-plus and depending underscore on startup
-__plus = null
-_plus = ->
- __plus ?= require 'underscore-plus'
-
-Delegato = require 'delegato'
-settings = require './settings'
-
-[
- CSON
- path
- selectList
- getEditorState # set by Base.init()
-] = [] # set null
-
-VMP_LOADING_FILE = null
-VMP_LOADED_FILES = []
-
-loadVmpOperationFile = (filename) ->
- # Call to loadVmpOperationFile can be nested.
- # 1. require("./operator-transform-string")
- # 2. in operator-transform-string.coffee call Base.getClass("Operator") cause operator.coffee required.
- # So we have to save original VMP_LOADING_FILE and restore it after require finished.
- vmpLoadingFileOriginal = VMP_LOADING_FILE
- VMP_LOADING_FILE = filename
- require(filename)
- VMP_LOADING_FILE = vmpLoadingFileOriginal
-
- VMP_LOADED_FILES.push(filename)
-
-OperationAbortedError = null
-
-vimStateMethods = [
- "onDidChangeSearch"
- "onDidConfirmSearch"
- "onDidCancelSearch"
- "onDidCommandSearch"
-
- # Life cycle of operationStack
- "onDidSetTarget", "emitDidSetTarget"
- "onWillSelectTarget", "emitWillSelectTarget"
- "onDidSelectTarget", "emitDidSelectTarget"
- "onDidFailSelectTarget", "emitDidFailSelectTarget"
-
- "onWillFinishMutation", "emitWillFinishMutation"
- "onDidFinishMutation", "emitDidFinishMutation"
- "onDidFinishOperation"
- "onDidResetOperationStack"
-
- "onDidSetOperatorModifier"
-
- "onWillActivateMode"
- "onDidActivateMode"
- "preemptWillDeactivateMode"
- "onWillDeactivateMode"
- "onDidDeactivateMode"
-
- "onDidCancelSelectList"
- "subscribe"
- "isMode"
- "getBlockwiseSelections"
- "getLastBlockwiseSelection"
- "addToClassList"
- "getConfig"
-]
-
-class Base
- Delegato.includeInto(this)
- @delegatesMethods(vimStateMethods..., toProperty: 'vimState')
- @delegatesProperty('mode', 'submode', 'swrap', 'utils', toProperty: 'vimState')
-
- constructor: (@vimState, properties=null) ->
- {@editor, @editorElement, @globalState, @swrap} = @vimState
- @name = @constructor.name
- Object.assign(this, properties) if properties?
-
- # To override
- initialize: ->
- resetState: ->
-
- # Operation processor execute only when isComplete() return true.
- # If false, operation processor postpone its execution.
- isComplete: ->
- if @requireInput and not @input?
- false
- else if @requireTarget
- # When this function is called in Base::constructor
- # tagert is still string like `MoveToRight`, in this case isComplete
- # is not available.
- @target?.isComplete?()
- else
- true
-
- requireTarget: false
- requireInput: false
- recordable: false
- repeated: false
- target: null # Set in Operator
- operator: null # Set in operator's target( Motion or TextObject )
-
- isAsTargetExceptSelectInVisualMode: ->
- @operator? and not @operator.instanceof('SelectInVisualMode')
-
- abort: ->
- OperationAbortedError ?= require './errors'
- throw new OperationAbortedError('aborted')
-
- # Count
- # -------------------------
- count: null
- defaultCount: 1
- getCount: (offset=0) ->
- @count ?= @vimState.getCount() ? @defaultCount
- @count + offset
-
- resetCount: ->
- @count = null
-
- isDefaultCount: ->
- @count is @defaultCount
-
- # Misc
- # -------------------------
- countTimes: (last, fn) ->
- return if last < 1
-
- stopped = false
- stop = -> stopped = true
- for count in [1..last]
- isFinal = count is last
- fn({count, isFinal, stop})
- break if stopped
-
- activateMode: (mode, submode) ->
- @onDidFinishOperation =>
- @vimState.activate(mode, submode)
-
- activateModeIfNecessary: (mode, submode) ->
- unless @vimState.isMode(mode, submode)
- @activateMode(mode, submode)
-
- new: (name, properties) ->
- klass = Base.getClass(name)
- new klass(@vimState, properties)
-
- # FIXME: This is used to clone Motion::Search to support `n` and `N`
- # But manual reseting and overriding property is bug prone.
- # Should extract as search spec object and use it by
- # creating clean instance of Search.
- clone: (vimState) ->
- properties = {}
- excludeProperties = ['editor', 'editorElement', 'globalState', 'vimState', 'operator']
- for own key, value of this when key not in excludeProperties
- properties[key] = value
- klass = this.constructor
- new klass(vimState, properties)
-
- cancelOperation: ->
- @vimState.operationStack.cancel(this)
-
- processOperation: ->
- @vimState.operationStack.process()
-
- focusSelectList: (options={}) ->
- @onDidCancelSelectList =>
- @cancelOperation()
- selectList ?= new (require './select-list')
- selectList.show(@vimState, options)
-
- input: null
- focusInput: (options = {}) ->
- options.onConfirm ?= (@input) => @processOperation()
- options.onCancel ?= => @cancelOperation()
- options.onChange ?= (input) => @vimState.hover.set(input)
- @vimState.focusInput(options)
-
- readChar: ->
- @vimState.readChar
- onConfirm: (@input) => @processOperation()
- onCancel: => @cancelOperation()
-
- getVimEofBufferPosition: ->
- @utils.getVimEofBufferPosition(@editor)
-
- getVimLastBufferRow: ->
- @utils.getVimLastBufferRow(@editor)
-
- getVimLastScreenRow: ->
- @utils.getVimLastScreenRow(@editor)
-
- getWordBufferRangeAndKindAtBufferPosition: (point, options) ->
- @utils.getWordBufferRangeAndKindAtBufferPosition(@editor, point, options)
-
- getFirstCharacterPositionForBufferRow: (row) ->
- @utils.getFirstCharacterPositionForBufferRow(@editor, row)
-
- getBufferRangeForRowRange: (rowRange) ->
- @utils.getBufferRangeForRowRange(@editor, rowRange)
-
- getIndentLevelForBufferRow: (row) ->
- @utils.getIndentLevelForBufferRow(@editor, row)
-
- scanForward: (args...) ->
- @utils.scanEditorInDirection(@editor, 'forward', args...)
-
- scanBackward: (args...) ->
- @utils.scanEditorInDirection(@editor, 'backward', args...)
-
- getFoldEndRowForRow: (args...) ->
- @utils.getFoldEndRowForRow(@editor, args...)
-
- instanceof: (klassName) ->
- this instanceof Base.getClass(klassName)
-
- is: (klassName) ->
- this.constructor is Base.getClass(klassName)
-
- isOperator: ->
- @constructor.operationKind is 'operator'
-
- isMotion: ->
- @constructor.operationKind is 'motion'
-
- isTextObject: ->
- @constructor.operationKind is 'text-object'
-
- getCursorBufferPosition: ->
- if @mode is 'visual'
- @getCursorPositionForSelection(@editor.getLastSelection())
- else
- @editor.getCursorBufferPosition()
-
- getCursorBufferPositions: ->
- if @mode is 'visual'
- @editor.getSelections().map(@getCursorPositionForSelection.bind(this))
- else
- @editor.getCursorBufferPositions()
-
- getBufferPositionForCursor: (cursor) ->
- if @mode is 'visual'
- @getCursorPositionForSelection(cursor.selection)
- else
- cursor.getBufferPosition()
-
- getCursorPositionForSelection: (selection) ->
- @swrap(selection).getBufferPositionFor('head', from: ['property', 'selection'])
-
- toString: ->
- str = @name
- if @target?
- str += ", target=#{@target.name}, target.wise=#{@target.wise} "
- else if @operator?
- str += ", wise=#{@wise} , operator=#{@operator.name}"
- else
- str
-
- # Class methods
- # -------------------------
- @writeCommandTableOnDisk: ->
- commandTable = @generateCommandTableByEagerLoad()
- _ = _plus()
- if _.isEqual(@commandTable, commandTable)
- atom.notifications.addInfo("No change commandTable", dismissable: true)
- return
-
- CSON ?= require 'season'
- path ?= require('path')
-
- loadableCSONText = """
- # This file is auto generated by `vim-mode-plus:write-command-table-on-disk` command.
- # DONT edit manually.
- module.exports =
- #{CSON.stringify(commandTable)}\n
- """
- commandTablePath = path.join(__dirname, "command-table.coffee")
- atom.workspace.open(commandTablePath).then (editor) ->
- editor.setText(loadableCSONText)
- editor.save()
- atom.notifications.addInfo("Updated commandTable", dismissable: true)
-
- @generateCommandTableByEagerLoad: ->
- # NOTE: changing order affects output of lib/command-table.coffee
- filesToLoad = [
- './operator', './operator-insert', './operator-transform-string',
- './motion', './motion-search', './text-object', './misc-command'
- ]
- filesToLoad.forEach(loadVmpOperationFile)
- _ = _plus()
- klasses = _.values(@getClassRegistry())
- klassesGroupedByFile = _.groupBy(klasses, (klass) -> klass.VMP_LOADING_FILE)
-
- commandTable = {}
- for file in filesToLoad
- for klass in klassesGroupedByFile[file]
- commandTable[klass.name] = klass.getSpec()
- commandTable
-
- @commandTable: null
- @init: (_getEditorState) ->
- getEditorState = _getEditorState
- @commandTable = require('./command-table')
- subscriptions = []
- for name, spec of @commandTable when spec.commandName?
- subscriptions.push(@registerCommandFromSpec(name, spec))
- return subscriptions
-
- classRegistry = {Base}
- @extend: (@command=true) ->
- @VMP_LOADING_FILE = VMP_LOADING_FILE
- if @name of classRegistry
- console.warn("Duplicate constructor #{@name}")
- classRegistry[@name] = this
-
- @initClass = @extend
-
- @getSpec: ->
- if @isCommand()
- file: @VMP_LOADING_FILE
- commandName: @getCommandName()
- commandScope: @getCommandScope()
- else
- file: @VMP_LOADING_FILE
-
- @getClass: (name) ->
- return klass if (klass = classRegistry[name])
-
- fileToLoad = @commandTable[name].file
- if fileToLoad not in VMP_LOADED_FILES
- if atom.inDevMode() and settings.get('debug')
- console.log "lazy-require: #{fileToLoad} for #{name}"
- loadVmpOperationFile(fileToLoad)
- return klass if (klass = classRegistry[name])
-
- throw new Error("class '#{name}' not found")
-
- @getClassRegistry: ->
- classRegistry
-
- @isCommand: ->
- @command
-
- @commandPrefix: 'vim-mode-plus'
- @getCommandName: ->
- @commandPrefix + ':' + _plus().dasherize(@name)
-
- @getCommandNameWithoutPrefix: ->
- _plus().dasherize(@name)
-
- @commandScope: 'atom-text-editor'
- @getCommandScope: ->
- @commandScope
-
- @getDesctiption: ->
- if @hasOwnProperty("description")
- @description
- else
- null
-
- @registerCommand: ->
- klass = this
- atom.commands.add @getCommandScope(), @getCommandName(), (event) ->
- vimState = getEditorState(@getModel()) ? getEditorState(atom.workspace.getActiveTextEditor())
- if vimState? # Possibly undefined See #85
- vimState.operationStack.run(klass)
- event.stopPropagation()
-
- @registerCommandFromSpec: (name, spec) ->
- {commandScope, commandPrefix, commandName, getClass} = spec
- commandScope ?= 'atom-text-editor'
- commandName ?= (commandPrefix ? 'vim-mode-plus') + ':' + _plus().dasherize(name)
- atom.commands.add commandScope, commandName, (event) ->
- vimState = getEditorState(@getModel()) ? getEditorState(atom.workspace.getActiveTextEditor())
- if vimState? # Possibly undefined See #85
- if getClass?
- vimState.operationStack.run(getClass(name))
- else
- vimState.operationStack.run(name)
- event.stopPropagation()
-
- # For demo-mode pkg integration
- @operationKind: null
- @getKindForCommandName: (command) ->
- command = command.replace(/^vim-mode-plus:/, "")
- _ = _plus()
- name = _.capitalize(_.camelize(command))
- if name of classRegistry
- classRegistry[name].operationKind
-
-module.exports = Base
diff --git a/lib/base.js b/lib/base.js
new file mode 100644
index 000000000..adf0ea506
--- /dev/null
+++ b/lib/base.js
@@ -0,0 +1,454 @@
+"use babel"
+
+const settings = require("./settings")
+
+let CSON, path, selectList, OperationAbortedError, __plus
+const CLASS_REGISTRY = {}
+
+function _plus() {
+ return __plus || (__plus = require("underscore-plus"))
+}
+
+let VMP_LOADING_FILE
+function loadVmpOperationFile(filename) {
+ // Call to loadVmpOperationFile can be nested.
+ // 1. require("./operator-transform-string")
+ // 2. in operator-transform-string.coffee call Base.getClass("Operator") cause operator.coffee required.
+ // So we have to save original VMP_LOADING_FILE and restore it after require finished.
+ const preserved = VMP_LOADING_FILE
+ VMP_LOADING_FILE = filename
+ require(filename)
+ VMP_LOADING_FILE = preserved
+}
+
+class Base {
+ static commandTable = null
+ static commandPrefix = "vim-mode-plus"
+ static commandScope = "atom-text-editor"
+ static operationKind = null
+ static getEditorState = null // set through init()
+
+ requireTarget = false
+ requireInput = false
+ recordable = false
+ repeated = false
+ target = null
+ operator = null
+ count = null
+ defaultCount = 1
+ input = null
+
+ get name() {
+ return this.constructor.name
+ }
+
+ constructor(vimState) {
+ this.vimState = vimState
+ }
+
+ // NOTE: initialize() must return `this`
+ initialize() {
+ return this
+ }
+
+ // Called both on cancel and success
+ resetState() {}
+
+ // Operation processor execute only when isComplete() return true.
+ // If false, operation processor postpone its execution.
+ isComplete() {
+ if (this.requireInput && this.input == null) {
+ return false
+ } else if (this.requireTarget) {
+ // When this function is called in Base::constructor
+ // tagert is still string like `MoveToRight`, in this case isComplete
+ // is not available.
+ return !!this.target && this.target.isComplete()
+ } else {
+ return true // Set in operator's target( Motion or TextObject )
+ }
+ }
+
+ isAsTargetExceptSelectInVisualMode() {
+ return this.operator && !this.operator.instanceof("SelectInVisualMode")
+ }
+
+ abort() {
+ if (!OperationAbortedError) OperationAbortedError = require("./errors")
+ throw new OperationAbortedError("aborted")
+ }
+
+ getCount(offset = 0) {
+ if (this.count == null) {
+ this.count = this.vimState.hasCount() ? this.vimState.getCount() : this.defaultCount
+ }
+ return this.count + offset
+ }
+
+ resetCount() {
+ this.count = null
+ }
+
+ countTimes(last, fn) {
+ if (last < 1) return
+
+ let stopped = false
+ const stop = () => (stopped = true)
+ for (let count = 1; count <= last; count++) {
+ fn({count, isFinal: count === last, stop})
+ if (stopped) break
+ }
+ }
+
+ activateMode(mode, submode) {
+ this.onDidFinishOperation(() => this.vimState.activate(mode, submode))
+ }
+
+ activateModeIfNecessary(mode, submode) {
+ if (!this.vimState.isMode(mode, submode)) {
+ this.activateMode(mode, submode)
+ }
+ }
+
+ getInstance(name, properties) {
+ return this.constructor.getInstance(this.vimState, name, properties)
+ }
+
+ cancelOperation() {
+ this.vimState.operationStack.cancel(this)
+ }
+
+ processOperation() {
+ this.vimState.operationStack.process()
+ }
+
+ focusSelectList(options = {}) {
+ this.onDidCancelSelectList(() => this.cancelOperation())
+ if (!selectList) {
+ selectList = new (require("./select-list"))()
+ }
+ selectList.show(this.vimState, options)
+ }
+
+ focusInput(options = {}) {
+ if (!options.onConfirm) {
+ options.onConfirm = input => {
+ this.input = input
+ this.processOperation()
+ }
+ }
+ if (!options.onCancel) {
+ options.onCancel = () => this.cancelOperation()
+ }
+ if (!options.onChange) {
+ options.onChange = input => this.vimState.hover.set(input)
+ }
+ this.vimState.focusInput(options)
+ }
+
+ readChar() {
+ this.vimState.readChar({
+ onConfirm: input => {
+ this.input = input
+ this.processOperation()
+ },
+ onCancel: () => this.cancelOperation(),
+ })
+ }
+
+ getVimEofBufferPosition() {
+ return this.utils.getVimEofBufferPosition(this.editor)
+ }
+
+ getVimLastBufferRow() {
+ return this.utils.getVimLastBufferRow(this.editor)
+ }
+
+ getVimLastScreenRow() {
+ return this.utils.getVimLastScreenRow(this.editor)
+ }
+
+ getWordBufferRangeAndKindAtBufferPosition(point, options) {
+ return this.utils.getWordBufferRangeAndKindAtBufferPosition(this.editor, point, options)
+ }
+
+ getFirstCharacterPositionForBufferRow(row) {
+ return this.utils.getFirstCharacterPositionForBufferRow(this.editor, row)
+ }
+
+ getBufferRangeForRowRange(rowRange) {
+ return this.utils.getBufferRangeForRowRange(this.editor, rowRange)
+ }
+
+ scanForward(...args) {
+ return this.utils.scanEditorInDirection(this.editor, "forward", ...args)
+ }
+
+ scanBackward(...args) {
+ return this.utils.scanEditorInDirection(this.editor, "backward", ...args)
+ }
+
+ getFoldStartRowForRow(...args) {
+ return this.utils.getFoldStartRowForRow(this.editor, ...args)
+ }
+
+ getFoldEndRowForRow(...args) {
+ return this.utils.getFoldEndRowForRow(this.editor, ...args)
+ }
+
+ instanceof(klassName) {
+ return this instanceof Base.getClass(klassName)
+ }
+
+ is(klassName) {
+ return this.constructor === Base.getClass(klassName)
+ }
+
+ isOperator() {
+ // Don't use `instanceof` to postpone require for faster activation.
+ return this.constructor.operationKind === "operator"
+ }
+
+ isMotion() {
+ // Don't use `instanceof` to postpone require for faster activation.
+ return this.constructor.operationKind === "motion"
+ }
+
+ isTextObject() {
+ // Don't use `instanceof` to postpone require for faster activation.
+ return this.constructor.operationKind === "text-object"
+ }
+
+ getCursorBufferPosition() {
+ return this.mode === "visual"
+ ? this.getCursorPositionForSelection(this.editor.getLastSelection())
+ : this.editor.getCursorBufferPosition()
+ }
+
+ getCursorBufferPositions() {
+ return this.mode === "visual"
+ ? this.editor.getSelections().map(this.getCursorPositionForSelection.bind(this))
+ : this.editor.getCursorBufferPositions()
+ }
+
+ getBufferPositionForCursor(cursor) {
+ return this.mode === "visual" ? this.getCursorPositionForSelection(cursor.selection) : cursor.getBufferPosition()
+ }
+
+ getCursorPositionForSelection(selection) {
+ return this.swrap(selection).getBufferPositionFor("head", {from: ["property", "selection"]})
+ }
+
+ toString() {
+ if (this.target) {
+ return `${this.name}, target=${this.target.name}, target.wise=${this.target.wise} `
+ } else if (this.operator) {
+ return `${this.name}, wise=${this.wise} , operator=${this.operator.name}`
+ } else {
+ return this.name
+ }
+ }
+
+ getCommandName() {
+ return this.constructor.getCommandName()
+ }
+
+ getCommandNameWithoutPrefix() {
+ return this.constructor.getCommandNameWithoutPrefix()
+ }
+
+ static writeCommandTableOnDisk() {
+ const commandTable = this.generateCommandTableByEagerLoad()
+ const _ = _plus()
+ if (_.isEqual(this.commandTable, commandTable)) {
+ atom.notifications.addInfo("No changes in commandTable", {dismissable: true})
+ return
+ }
+
+ if (!CSON) CSON = require("season")
+ if (!path) path = require("path")
+
+ let loadableCSONText = "# This file is auto generated by `vim-mode-plus:write-command-table-on-disk` command.\n"
+ loadableCSONText += "# DONT edit manually.\n"
+ loadableCSONText += CSON.stringify(commandTable) + "\n"
+
+ const commandTablePath = path.join(__dirname, "command-table.coffee")
+ atom.workspace.open(commandTablePath).then(editor => {
+ editor.setText(loadableCSONText)
+ editor.save()
+ atom.notifications.addInfo("Updated commandTable", {dismissable: true})
+ })
+ }
+
+ static generateCommandTableByEagerLoad() {
+ // NOTE: changing order affects output of lib/command-table.coffee
+ const filesToLoad = [
+ "./operator",
+ "./operator-insert",
+ "./operator-transform-string",
+ "./motion",
+ "./motion-search",
+ "./text-object",
+ "./misc-command",
+ ]
+ filesToLoad.forEach(loadVmpOperationFile)
+ const _ = _plus()
+ const klassesGroupedByFile = _.groupBy(_.values(CLASS_REGISTRY), klass => klass.file)
+
+ const commandTable = {}
+ for (const file of filesToLoad) {
+ for (const klass of klassesGroupedByFile[file]) {
+ commandTable[klass.name] = klass.isCommand()
+ ? {file: klass.file, commandName: klass.getCommandName(), commandScope: klass.getCommandScope()}
+ : {file: klass.file}
+ }
+ }
+ return commandTable
+ }
+
+ static init(getEditorState) {
+ this.getEditorState = getEditorState
+
+ this.commandTable = require("./command-table")
+ const subscriptions = []
+ for (const name in this.commandTable) {
+ const spec = this.commandTable[name]
+ if (spec.commandName) {
+ subscriptions.push(this.registerCommandFromSpec(name, spec))
+ }
+ }
+ return subscriptions
+ }
+
+ static register(command = true) {
+ this.command = command
+ this.file = VMP_LOADING_FILE
+ if (this.name in CLASS_REGISTRY) {
+ console.warn(`Duplicate constructor ${this.name}`)
+ }
+ CLASS_REGISTRY[this.name] = this
+ }
+
+ static extend(...args) {
+ console.error("calling deprecated Base.extend(), use Base.register instead!")
+ this.register(...args)
+ }
+
+ static getClass(name) {
+ if (name in CLASS_REGISTRY) return CLASS_REGISTRY[name]
+
+ const fileToLoad = this.commandTable[name].file
+ if (atom.inDevMode() && settings.get("debug")) {
+ console.log(`lazy-require: ${fileToLoad} for ${name}`)
+ }
+ loadVmpOperationFile(fileToLoad)
+ if (name in CLASS_REGISTRY) return CLASS_REGISTRY[name]
+
+ throw new Error(`class '${name}' not found`)
+ }
+
+ static getInstance(vimState, klassOrName, properties) {
+ const klass = typeof klassOrName === "function" ? klassOrName : Base.getClass(klassOrName)
+ const instance = new klass(vimState)
+ if (properties) Object.assign(instance, properties)
+ return instance.initialize() // initialize must return instance.
+ }
+
+ static getClassRegistry() {
+ return CLASS_REGISTRY
+ }
+
+ static isCommand() {
+ return this.command
+ }
+
+ static getCommandName() {
+ return this.commandPrefix + ":" + _plus().dasherize(this.name)
+ }
+
+ static getCommandNameWithoutPrefix() {
+ return _plus().dasherize(this.name)
+ }
+
+ static getCommandScope() {
+ return this.commandScope
+ }
+
+ static registerCommand() {
+ return this.registerCommandFromSpec(this.name, {
+ commandScope: this.getCommandScope(),
+ commandName: this.getCommandName(),
+ getClass: () => this,
+ })
+ }
+
+ static registerCommandFromSpec(name, spec) {
+ let {commandScope = "atom-text-editor", commandPrefix = "vim-mode-plus", commandName, getClass} = spec
+ if (!commandName) commandName = commandPrefix + ":" + _plus().dasherize(name)
+ if (!getClass) getClass = name => this.getClass(name)
+
+ const getEditorState = this.getEditorState
+ return atom.commands.add(commandScope, commandName, function(event) {
+ const vimState = getEditorState(this.getModel()) || getEditorState(atom.workspace.getActiveTextEditor())
+ if (vimState) vimState.operationStack.run(getClass(name)) // vimState possibly be undefined See #85
+ event.stopPropagation()
+ })
+ }
+
+ static getKindForCommandName(command) {
+ const commandWithoutPrefix = command.replace(/^vim-mode-plus:/, "")
+ const {capitalize, camelize} = _plus()
+ const commandClassName = capitalize(camelize(commandWithoutPrefix))
+ if (commandClassName in CLASS_REGISTRY) {
+ return CLASS_REGISTRY[commandClassName].operationKind
+ }
+ }
+
+ // Proxy propperties and methods
+ //===========================================================================
+ get mode() { return this.vimState.mode } // prettier-ignore
+ get submode() { return this.vimState.submode } // prettier-ignore
+ get swrap() { return this.vimState.swrap } // prettier-ignore
+ get utils() { return this.vimState.utils } // prettier-ignore
+ get editor() { return this.vimState.editor } // prettier-ignore
+ get editorElement() { return this.vimState.editorElement } // prettier-ignore
+ get globalState() { return this.vimState.globalState } // prettier-ignore
+ get mutationManager() { return this.vimState.mutationManager } // prettier-ignore
+ get occurrenceManager() { return this.vimState.occurrenceManager } // prettier-ignore
+ get persistentSelection() { return this.vimState.persistentSelection } // prettier-ignore
+
+ onDidChangeSearch(...args) { return this.vimState.onDidChangeSearch(...args) } // prettier-ignore
+ onDidConfirmSearch(...args) { return this.vimState.onDidConfirmSearch(...args) } // prettier-ignore
+ onDidCancelSearch(...args) { return this.vimState.onDidCancelSearch(...args) } // prettier-ignore
+ onDidCommandSearch(...args) { return this.vimState.onDidCommandSearch(...args) } // prettier-ignore
+ onDidSetTarget(...args) { return this.vimState.onDidSetTarget(...args) } // prettier-ignore
+ emitDidSetTarget(...args) { return this.vimState.emitDidSetTarget(...args) } // prettier-ignore
+ onWillSelectTarget(...args) { return this.vimState.onWillSelectTarget(...args) } // prettier-ignore
+ emitWillSelectTarget(...args) { return this.vimState.emitWillSelectTarget(...args) } // prettier-ignore
+ onDidSelectTarget(...args) { return this.vimState.onDidSelectTarget(...args) } // prettier-ignore
+ emitDidSelectTarget(...args) { return this.vimState.emitDidSelectTarget(...args) } // prettier-ignore
+ onDidFailSelectTarget(...args) { return this.vimState.onDidFailSelectTarget(...args) } // prettier-ignore
+ emitDidFailSelectTarget(...args) { return this.vimState.emitDidFailSelectTarget(...args) } // prettier-ignore
+ onWillFinishMutation(...args) { return this.vimState.onWillFinishMutation(...args) } // prettier-ignore
+ emitWillFinishMutation(...args) { return this.vimState.emitWillFinishMutation(...args) } // prettier-ignore
+ onDidFinishMutation(...args) { return this.vimState.onDidFinishMutation(...args) } // prettier-ignore
+ emitDidFinishMutation(...args) { return this.vimState.emitDidFinishMutation(...args) } // prettier-ignore
+ onDidFinishOperation(...args) { return this.vimState.onDidFinishOperation(...args) } // prettier-ignore
+ onDidResetOperationStack(...args) { return this.vimState.onDidResetOperationStack(...args) } // prettier-ignore
+ onDidSetOperatorModifier(...args) { return this.vimState.onDidSetOperatorModifier(...args) } // prettier-ignore
+ onWillActivateMode(...args) { return this.vimState.onWillActivateMode(...args) } // prettier-ignore
+ onDidActivateMode(...args) { return this.vimState.onDidActivateMode(...args) } // prettier-ignore
+ preemptWillDeactivateMode(...args) { return this.vimState.preemptWillDeactivateMode(...args) } // prettier-ignore
+ onWillDeactivateMode(...args) { return this.vimState.onWillDeactivateMode(...args) } // prettier-ignore
+ onDidDeactivateMode(...args) { return this.vimState.onDidDeactivateMode(...args) } // prettier-ignore
+ onDidCancelSelectList(...args) { return this.vimState.onDidCancelSelectList(...args) } // prettier-ignore
+ subscribe(...args) { return this.vimState.subscribe(...args) } // prettier-ignore
+ isMode(...args) { return this.vimState.isMode(...args) } // prettier-ignore
+ getBlockwiseSelections(...args) { return this.vimState.getBlockwiseSelections(...args) } // prettier-ignore
+ getLastBlockwiseSelection(...args) { return this.vimState.getLastBlockwiseSelection(...args) } // prettier-ignore
+ addToClassList(...args) { return this.vimState.addToClassList(...args) } // prettier-ignore
+ getConfig(...args) { return this.vimState.getConfig(...args) } // prettier-ignore
+}
+Base.register(false)
+
+module.exports = Base
diff --git a/lib/blockwise-selection.js b/lib/blockwise-selection.js
index 17c516e2d..0797dd3e8 100644
--- a/lib/blockwise-selection.js
+++ b/lib/blockwise-selection.js
@@ -1,12 +1,12 @@
const _ = require("underscore-plus")
-const {sortRanges, assertWithException, trimRange, getRange} = require("./utils")
+const {sortRanges, assertWithException, trimRange, getList} = require("./utils")
const settings = require("./settings")
const blockwiseSelectionsByEditor = new Map()
let __swrap
function swrap(...args) {
- if (__swrap == null) __swrap = require("./selection-wrapper")
+ if (!__swrap) __swrap = require("./selection-wrapper")
return __swrap(...args)
}
@@ -80,7 +80,7 @@ module.exports = class BlockwiseSelection {
endColumn = end.column + 1
}
- const ranges = getRange(start.row, end.row).map(row => [[row, startColumn], [row, endColumn]])
+ const ranges = getList(start.row, end.row).map(row => [[row, startColumn], [row, endColumn]])
selection.setBufferRange(ranges.shift(), {reversed})
this.selections = [selection, ...ranges.map(range => this.editor.addSelectionForBufferRange(range, {reversed}))]
}
diff --git a/lib/command-table.coffee b/lib/command-table.coffee
index ce58dfac8..9786c6f1c 100644
--- a/lib/command-table.coffee
+++ b/lib/command-table.coffee
@@ -673,7 +673,7 @@ MoveToLineByPercent:
commandScope: "atom-text-editor"
MoveToRelativeLine:
file: "./motion"
-MoveToRelativeLineMinimumOne:
+MoveToRelativeLineMinimumTwo:
file: "./motion"
MoveToTopOfScreen:
file: "./motion"
diff --git a/lib/developer.js b/lib/developer.js
index 12ffbab89..237a65e1d 100644
--- a/lib/developer.js
+++ b/lib/developer.js
@@ -208,7 +208,7 @@ module.exports = class Developer {
ancestors.pop()
const kind = ancestors.pop().name
const commandName = klass.getCommandName()
- const description = klass.getDesctiption() ? klass.getDesctiption().replace(/\n/g, "
") : undefined
+ // const description = klass.getDesctiption() ? klass.getDesctiption().replace(/\n/g, "
") : undefined
const keymaps = getKeyBindingForCommand(commandName, {packageName: "vim-mode-plus"})
const keymap = keymaps
diff --git a/lib/main.js b/lib/main.js
index 3493010e5..58c0a7414 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -21,6 +21,7 @@ module.exports = {
activate() {
this.emitter = new Emitter()
settings.notifyDeprecatedParams()
+ settings.notifyCoffeeScriptNoLongerSupportedToExtendVMP()
settings.migrateRenamedParams()
if (atom.inSpecMode()) settings.set("strictAssertion", true)
@@ -249,7 +250,7 @@ module.exports = {
provideVimModePlus() {
return {
Base: Base,
- registerCommandFromSpec: Base.registerCommandFromSpec,
+ registerCommandFromSpec: Base.registerCommandFromSpec.bind(Base),
getGlobalState: this.getGlobalState,
getEditorState: this.getEditorState,
observeVimStates: this.observeVimStates.bind(this),
diff --git a/lib/mark-manager.js b/lib/mark-manager.js
index 3e1abf2d8..edd71a5c1 100644
--- a/lib/mark-manager.js
+++ b/lib/mark-manager.js
@@ -40,7 +40,7 @@ class MarkManager {
const {editor} = this.vimState
point = editor.clipBufferPosition(point)
this.marks[name] = this.markerLayer.markBufferPosition(point, {invalidate: "never"})
- this.vimState.emitter.emit("did-set-mark", {name, point, editor})
+ this.vimState.emitter.emit("did-set-mark", {name, bufferPosition: point, editor})
}
}
diff --git a/lib/misc-command.coffee b/lib/misc-command.coffee
deleted file mode 100644
index 15e09a4ce..000000000
--- a/lib/misc-command.coffee
+++ /dev/null
@@ -1,503 +0,0 @@
-{Range, Point} = require 'atom'
-Base = require './base'
-_ = require 'underscore-plus'
-
-{
- moveCursorRight
- isLinewiseRange
- setBufferRow
- sortRanges
- findRangeContainsPoint
- isSingleLineRange
- isLeadingWhiteSpaceRange
- humanizeBufferRange
- getFoldInfoByKind
- limitNumber
- getFoldRowRangesContainedByFoldStartsAtRow
-} = require './utils'
-
-class MiscCommand extends Base
- @extend(false)
- @operationKind: 'misc-command'
- constructor: ->
- super
- @initialize()
-
-class Mark extends MiscCommand
- @extend()
- requireInput: true
- initialize: ->
- @readChar()
- super
-
- execute: ->
- @vimState.mark.set(@input, @editor.getCursorBufferPosition())
- @activateMode('normal')
-
-class ReverseSelections extends MiscCommand
- @extend()
- execute: ->
- @swrap.setReversedState(@editor, not @editor.getLastSelection().isReversed())
- if @isMode('visual', 'blockwise')
- @getLastBlockwiseSelection().autoscroll()
-
-class BlockwiseOtherEnd extends ReverseSelections
- @extend()
- execute: ->
- for blockwiseSelection in @getBlockwiseSelections()
- blockwiseSelection.reverse()
- super
-
-class Undo extends MiscCommand
- @extend()
-
- setCursorPosition: ({newRanges, oldRanges, strategy}) ->
- lastCursor = @editor.getLastCursor() # This is restored cursor
-
- if strategy is 'smart'
- changedRange = findRangeContainsPoint(newRanges, lastCursor.getBufferPosition())
- else
- changedRange = sortRanges(newRanges.concat(oldRanges))[0]
-
- if changedRange?
- if isLinewiseRange(changedRange)
- setBufferRow(lastCursor, changedRange.start.row)
- else
- lastCursor.setBufferPosition(changedRange.start)
-
- mutateWithTrackChanges: ->
- newRanges = []
- oldRanges = []
-
- # Collect changed range while mutating text-state by fn callback.
- disposable = @editor.getBuffer().onDidChange ({newRange, oldRange}) ->
- if newRange.isEmpty()
- oldRanges.push(oldRange) # Remove only
- else
- newRanges.push(newRange)
-
- @mutate()
-
- disposable.dispose()
- {newRanges, oldRanges}
-
- flashChanges: ({newRanges, oldRanges}) ->
- isMultipleSingleLineRanges = (ranges) ->
- ranges.length > 1 and ranges.every(isSingleLineRange)
-
- if newRanges.length > 0
- return if @isMultipleAndAllRangeHaveSameColumnAndConsecutiveRows(newRanges)
- newRanges = newRanges.map (range) => humanizeBufferRange(@editor, range)
- newRanges = @filterNonLeadingWhiteSpaceRange(newRanges)
-
- if isMultipleSingleLineRanges(newRanges)
- @flash(newRanges, type: 'undo-redo-multiple-changes')
- else
- @flash(newRanges, type: 'undo-redo')
- else
- return if @isMultipleAndAllRangeHaveSameColumnAndConsecutiveRows(oldRanges)
-
- if isMultipleSingleLineRanges(oldRanges)
- oldRanges = @filterNonLeadingWhiteSpaceRange(oldRanges)
- @flash(oldRanges, type: 'undo-redo-multiple-delete')
-
- filterNonLeadingWhiteSpaceRange: (ranges) ->
- ranges.filter (range) =>
- not isLeadingWhiteSpaceRange(@editor, range)
-
- # [TODO] Improve further by checking oldText, newText?
- # [Purpose of this is function]
- # Suppress flash when undo/redoing toggle-comment while flashing undo/redo of occurrence operation.
- # This huristic approach never be perfect.
- # Ultimately cannnot distinguish occurrence operation.
- isMultipleAndAllRangeHaveSameColumnAndConsecutiveRows: (ranges) ->
- return false if ranges.length <= 1
-
- {start: {column: startColumn}, end: {column: endColumn}} = ranges[0]
- previousRow = null
- for range in ranges
- {start, end} = range
- unless ((start.column is startColumn) and (end.column is endColumn))
- return false
-
- if previousRow? and (previousRow + 1 isnt start.row)
- return false
- previousRow = start.row
- return true
-
- ranges.every ({start, end}) ->
- (start.column is startColumn) and (end.column is endColumn)
-
- flash: (flashRanges, options) ->
- options.timeout ?= 500
- @onDidFinishOperation =>
- @vimState.flash(flashRanges, options)
-
- execute: ->
- {newRanges, oldRanges} = @mutateWithTrackChanges()
-
- for selection in @editor.getSelections()
- selection.clear()
-
- if @getConfig('setCursorToStartOfChangeOnUndoRedo')
- strategy = @getConfig('setCursorToStartOfChangeOnUndoRedoStrategy')
- @setCursorPosition({newRanges, oldRanges, strategy})
- @vimState.clearSelections()
-
- if @getConfig('flashOnUndoRedo')
- @flashChanges({newRanges, oldRanges})
-
- @activateMode('normal')
-
- mutate: ->
- @editor.undo()
-
-class Redo extends Undo
- @extend()
- mutate: ->
- @editor.redo()
-
-# zc
-class FoldCurrentRow extends MiscCommand
- @extend()
- execute: ->
- for selection in @editor.getSelections()
- {row} = @getCursorPositionForSelection(selection)
- @editor.foldBufferRow(row)
-
-# zo
-class UnfoldCurrentRow extends MiscCommand
- @extend()
- execute: ->
- for selection in @editor.getSelections()
- {row} = @getCursorPositionForSelection(selection)
- @editor.unfoldBufferRow(row)
-
-# za
-class ToggleFold extends MiscCommand
- @extend()
- execute: ->
- point = @editor.getCursorBufferPosition()
- @editor.toggleFoldAtBufferRow(point.row)
-
-# Base of zC, zO, zA
-class FoldCurrentRowRecursivelyBase extends MiscCommand
- @extend(false)
-
- foldRecursively: (row) ->
- rowRanges = getFoldRowRangesContainedByFoldStartsAtRow(@editor, row)
- if rowRanges?
- startRows = rowRanges.map (rowRange) -> rowRange[0]
- for row in startRows.reverse() when not @editor.isFoldedAtBufferRow(row)
- @editor.foldBufferRow(row)
-
- unfoldRecursively: (row) ->
- rowRanges = getFoldRowRangesContainedByFoldStartsAtRow(@editor, row)
- if rowRanges?
- startRows = rowRanges.map (rowRange) -> rowRange[0]
- for row in startRows when @editor.isFoldedAtBufferRow(row)
- @editor.unfoldBufferRow(row)
-
- foldRecursivelyForAllSelections: ->
- for selection in @editor.getSelectionsOrderedByBufferPosition().reverse()
- @foldRecursively(@getCursorPositionForSelection(selection).row)
-
- unfoldRecursivelyForAllSelections: ->
- for selection in @editor.getSelectionsOrderedByBufferPosition()
- @unfoldRecursively(@getCursorPositionForSelection(selection).row)
-
-# zC
-class FoldCurrentRowRecursively extends FoldCurrentRowRecursivelyBase
- @extend()
- execute: ->
- @foldRecursivelyForAllSelections()
-
-# zO
-class UnfoldCurrentRowRecursively extends FoldCurrentRowRecursivelyBase
- @extend()
- execute: ->
- @unfoldRecursivelyForAllSelections()
-
-# zA
-class ToggleFoldRecursively extends FoldCurrentRowRecursivelyBase
- @extend()
- execute: ->
- row = @getCursorPositionForSelection(@editor.getLastSelection()).row
- if @editor.isFoldedAtBufferRow(row)
- @unfoldRecursivelyForAllSelections()
- else
- @foldRecursivelyForAllSelections()
-
-# zR
-class UnfoldAll extends MiscCommand
- @extend()
- execute: ->
- @editor.unfoldAll()
-
-# zM
-class FoldAll extends MiscCommand
- @extend()
- execute: ->
- {allFold} = getFoldInfoByKind(@editor)
- if allFold?
- @editor.unfoldAll()
- for {indent, startRow, endRow} in allFold.rowRangesWithIndent
- if indent <= @getConfig('maxFoldableIndentLevel')
- @editor.foldBufferRowRange(startRow, endRow)
-
-# zr
-class UnfoldNextIndentLevel extends MiscCommand
- @extend()
- execute: ->
- {folded} = getFoldInfoByKind(@editor)
- if folded?
- {minIndent, rowRangesWithIndent} = folded
- count = limitNumber(@getCount() - 1, min: 0)
- targetIndents = [minIndent..(minIndent + count)]
- for {indent, startRow} in rowRangesWithIndent
- if indent in targetIndents
- @editor.unfoldBufferRow(startRow)
-
-# zm
-class FoldNextIndentLevel extends MiscCommand
- @extend()
- execute: ->
- {unfolded, allFold} = getFoldInfoByKind(@editor)
- if unfolded?
- # FIXME: Why I need unfoldAll()? Why can't I just fold non-folded-fold only?
- # Unless unfoldAll() here, @editor.unfoldAll() delete foldMarker but fail
- # to render unfolded rows correctly.
- # I believe this is bug of text-buffer's markerLayer which assume folds are
- # created **in-order** from top-row to bottom-row.
- @editor.unfoldAll()
-
- maxFoldable = @getConfig('maxFoldableIndentLevel')
- fromLevel = Math.min(unfolded.maxIndent, maxFoldable)
- count = limitNumber(@getCount() - 1, min: 0)
- fromLevel = limitNumber(fromLevel - count, min: 0)
- targetIndents = [fromLevel..maxFoldable]
-
- for {indent, startRow, endRow} in allFold.rowRangesWithIndent
- if indent in targetIndents
- @editor.foldBufferRowRange(startRow, endRow)
-
-class ReplaceModeBackspace extends MiscCommand
- @commandScope: 'atom-text-editor.vim-mode-plus.insert-mode.replace'
- @extend()
- execute: ->
- for selection in @editor.getSelections()
- # char might be empty.
- char = @vimState.modeManager.getReplacedCharForSelection(selection)
- if char?
- selection.selectLeft()
- unless selection.insertText(char).isEmpty()
- selection.cursor.moveLeft()
-
-class ScrollWithoutChangingCursorPosition extends MiscCommand
- @extend(false)
- scrolloff: 2 # atom default. Better to use editor.getVerticalScrollMargin()?
- cursorPixel: null
-
- getFirstVisibleScreenRow: ->
- @editorElement.getFirstVisibleScreenRow()
-
- getLastVisibleScreenRow: ->
- @editorElement.getLastVisibleScreenRow()
-
- getLastScreenRow: ->
- @editor.getLastScreenRow()
-
- getCursorPixel: ->
- point = @editor.getCursorScreenPosition()
- @editorElement.pixelPositionForScreenPosition(point)
-
-# ctrl-e scroll lines downwards
-class ScrollDown extends ScrollWithoutChangingCursorPosition
- @extend()
-
- execute: ->
- count = @getCount()
- oldFirstRow = @editor.getFirstVisibleScreenRow()
- @editor.setFirstVisibleScreenRow(oldFirstRow + count)
- newFirstRow = @editor.getFirstVisibleScreenRow()
-
- offset = 2
- {row, column} = @editor.getCursorScreenPosition()
- if row < (newFirstRow + offset)
- newPoint = [row + count, column]
- @editor.setCursorScreenPosition(newPoint, autoscroll: false)
-
-# ctrl-y scroll lines upwards
-class ScrollUp extends ScrollWithoutChangingCursorPosition
- @extend()
-
- execute: ->
- count = @getCount()
- oldFirstRow = @editor.getFirstVisibleScreenRow()
- @editor.setFirstVisibleScreenRow(oldFirstRow - count)
- newLastRow = @editor.getLastVisibleScreenRow()
-
- offset = 2
- {row, column} = @editor.getCursorScreenPosition()
- if row >= (newLastRow - offset)
- newPoint = [row - count, column]
- @editor.setCursorScreenPosition(newPoint, autoscroll: false)
-
-# ScrollWithoutChangingCursorPosition without Cursor Position change.
-# -------------------------
-class ScrollCursor extends ScrollWithoutChangingCursorPosition
- @extend(false)
- execute: ->
- @moveToFirstCharacterOfLine?()
- if @isScrollable()
- @editorElement.setScrollTop @getScrollTop()
-
- moveToFirstCharacterOfLine: ->
- @editor.moveToFirstCharacterOfLine()
-
- getOffSetPixelHeight: (lineDelta=0) ->
- @editor.getLineHeightInPixels() * (@scrolloff + lineDelta)
-
-# z enter
-class ScrollCursorToTop extends ScrollCursor
- @extend()
- isScrollable: ->
- @getLastVisibleScreenRow() isnt @getLastScreenRow()
-
- getScrollTop: ->
- @getCursorPixel().top - @getOffSetPixelHeight()
-
-# zt
-class ScrollCursorToTopLeave extends ScrollCursorToTop
- @extend()
- moveToFirstCharacterOfLine: null
-
-# z-
-class ScrollCursorToBottom extends ScrollCursor
- @extend()
- isScrollable: ->
- @getFirstVisibleScreenRow() isnt 0
-
- getScrollTop: ->
- @getCursorPixel().top - (@editorElement.getHeight() - @getOffSetPixelHeight(1))
-
-# zb
-class ScrollCursorToBottomLeave extends ScrollCursorToBottom
- @extend()
- moveToFirstCharacterOfLine: null
-
-# z.
-class ScrollCursorToMiddle extends ScrollCursor
- @extend()
- isScrollable: ->
- true
-
- getScrollTop: ->
- @getCursorPixel().top - (@editorElement.getHeight() / 2)
-
-# zz
-class ScrollCursorToMiddleLeave extends ScrollCursorToMiddle
- @extend()
- moveToFirstCharacterOfLine: null
-
-# Horizontal ScrollWithoutChangingCursorPosition
-# -------------------------
-# zs
-class ScrollCursorToLeft extends ScrollWithoutChangingCursorPosition
- @extend()
-
- execute: ->
- @editorElement.setScrollLeft(@getCursorPixel().left)
-
-# ze
-class ScrollCursorToRight extends ScrollCursorToLeft
- @extend()
-
- execute: ->
- @editorElement.setScrollRight(@getCursorPixel().left)
-
-# insert-mode specific commands
-# -------------------------
-class InsertMode extends MiscCommand
- @commandScope: 'atom-text-editor.vim-mode-plus.insert-mode'
-
-class ActivateNormalModeOnce extends InsertMode
- @extend()
- thisCommandName: @getCommandName()
-
- execute: ->
- cursorsToMoveRight = @editor.getCursors().filter (cursor) -> not cursor.isAtBeginningOfLine()
- @vimState.activate('normal')
- moveCursorRight(cursor) for cursor in cursorsToMoveRight
- disposable = atom.commands.onDidDispatch ({type}) =>
- return if type is @thisCommandName
- disposable.dispose()
- disposable = null
- @vimState.activate('insert')
-
-class InsertRegister extends InsertMode
- @extend()
- requireInput: true
-
- initialize: ->
- super
- @readChar()
-
- execute: ->
- @editor.transact =>
- for selection in @editor.getSelections()
- text = @vimState.register.getText(@input, selection)
- selection.insertText(text)
-
-class InsertLastInserted extends InsertMode
- @extend()
- @description: """
- Insert text inserted in latest insert-mode.
- Equivalent to *i_CTRL-A* of pure Vim
- """
- execute: ->
- text = @vimState.register.getText('.')
- @editor.insertText(text)
-
-class CopyFromLineAbove extends InsertMode
- @extend()
- @description: """
- Insert character of same-column of above line.
- Equivalent to *i_CTRL-Y* of pure Vim
- """
- rowDelta: -1
-
- execute: ->
- translation = [@rowDelta, 0]
- @editor.transact =>
- for selection in @editor.getSelections()
- point = selection.cursor.getBufferPosition().translate(translation)
- continue if point.row < 0
- range = Range.fromPointWithDelta(point, 0, 1)
- if text = @editor.getTextInBufferRange(range)
- selection.insertText(text)
-
-class CopyFromLineBelow extends CopyFromLineAbove
- @extend()
- @description: """
- Insert character of same-column of above line.
- Equivalent to *i_CTRL-E* of pure Vim
- """
- rowDelta: +1
-
-class NextTab extends MiscCommand
- @extend()
- defaultCount: 0
- execute: ->
- count = @getCount()
- pane = atom.workspace.paneForItem(@editor)
- if count
- pane.activateItemAtIndex(count - 1)
- else
- pane.activateNextItem()
-
-class PreviousTab extends MiscCommand
- @extend()
- execute: ->
- pane = atom.workspace.paneForItem(@editor)
- pane.activatePreviousItem()
diff --git a/lib/misc-command.js b/lib/misc-command.js
new file mode 100644
index 000000000..1677d354e
--- /dev/null
+++ b/lib/misc-command.js
@@ -0,0 +1,591 @@
+"use babel"
+
+const {Range} = require("atom")
+const Base = require("./base")
+const {
+ moveCursorRight,
+ isLinewiseRange,
+ setBufferRow,
+ sortRanges,
+ findRangeContainsPoint,
+ isSingleLineRange,
+ isLeadingWhiteSpaceRange,
+ humanizeBufferRange,
+ getFoldInfoByKind,
+ limitNumber,
+ getFoldRowRangesContainedByFoldStartsAtRow,
+ getList,
+} = require("./utils")
+
+class MiscCommand extends Base {
+ static operationKind = "misc-command"
+}
+MiscCommand.register(false)
+
+class Mark extends MiscCommand {
+ requireInput = true
+ initialize() {
+ this.readChar()
+ return super.initialize()
+ }
+
+ execute() {
+ this.vimState.mark.set(this.input, this.editor.getCursorBufferPosition())
+ this.activateMode("normal")
+ }
+}
+Mark.register()
+
+class ReverseSelections extends MiscCommand {
+ execute() {
+ this.swrap.setReversedState(this.editor, !this.editor.getLastSelection().isReversed())
+ if (this.isMode("visual", "blockwise")) {
+ this.getLastBlockwiseSelection().autoscroll()
+ }
+ }
+}
+ReverseSelections.register()
+
+class BlockwiseOtherEnd extends ReverseSelections {
+ execute() {
+ for (const blockwiseSelection of this.getBlockwiseSelections()) {
+ blockwiseSelection.reverse()
+ }
+ super.execute()
+ }
+}
+BlockwiseOtherEnd.register()
+
+class Undo extends MiscCommand {
+ setCursorPosition({newRanges, oldRanges, strategy}) {
+ const lastCursor = this.editor.getLastCursor() // This is restored cursor
+
+ const changedRange =
+ strategy === "smart"
+ ? findRangeContainsPoint(newRanges, lastCursor.getBufferPosition())
+ : sortRanges(newRanges.concat(oldRanges))[0]
+
+ if (changedRange) {
+ if (isLinewiseRange(changedRange)) setBufferRow(lastCursor, changedRange.start.row)
+ else lastCursor.setBufferPosition(changedRange.start)
+ }
+ }
+
+ mutateWithTrackChanges() {
+ const newRanges = []
+ const oldRanges = []
+
+ // Collect changed range while mutating text-state by fn callback.
+ const disposable = this.editor.getBuffer().onDidChange(({newRange, oldRange}) => {
+ if (newRange.isEmpty()) {
+ oldRanges.push(oldRange) // Remove only
+ } else {
+ newRanges.push(newRange)
+ }
+ })
+
+ this.mutate()
+ disposable.dispose()
+ return {newRanges, oldRanges}
+ }
+
+ flashChanges({newRanges, oldRanges}) {
+ const isMultipleSingleLineRanges = ranges => ranges.length > 1 && ranges.every(isSingleLineRange)
+
+ if (newRanges.length > 0) {
+ if (this.isMultipleAndAllRangeHaveSameColumnAndConsecutiveRows(newRanges)) return
+
+ newRanges = newRanges.map(range => humanizeBufferRange(this.editor, range))
+ newRanges = this.filterNonLeadingWhiteSpaceRange(newRanges)
+
+ const type = isMultipleSingleLineRanges(newRanges) ? "undo-redo-multiple-changes" : "undo-redo"
+ this.flash(newRanges, {type})
+ } else {
+ if (this.isMultipleAndAllRangeHaveSameColumnAndConsecutiveRows(oldRanges)) return
+
+ if (isMultipleSingleLineRanges(oldRanges)) {
+ oldRanges = this.filterNonLeadingWhiteSpaceRange(oldRanges)
+ this.flash(oldRanges, {type: "undo-redo-multiple-delete"})
+ }
+ }
+ }
+
+ filterNonLeadingWhiteSpaceRange(ranges) {
+ return ranges.filter(range => !isLeadingWhiteSpaceRange(this.editor, range))
+ }
+
+ // [TODO] Improve further by checking oldText, newText?
+ // [Purpose of this function]
+ // Suppress flash when undo/redoing toggle-comment while flashing undo/redo of occurrence operation.
+ // This huristic approach never be perfect.
+ // Ultimately cannnot distinguish occurrence operation.
+ isMultipleAndAllRangeHaveSameColumnAndConsecutiveRows(ranges) {
+ if (ranges.length <= 1) {
+ return false
+ }
+
+ const {start: {column: startColumn}, end: {column: endColumn}} = ranges[0]
+ let previousRow
+
+ for (const range of ranges) {
+ const {start, end} = range
+ if (start.column !== startColumn || end.column !== endColumn) return false
+ if (previousRow != null && previousRow + 1 !== start.row) return false
+ previousRow = start.row
+ }
+ return true
+ }
+
+ flash(ranges, options) {
+ if (options.timeout == null) options.timeout = 500
+ this.onDidFinishOperation(() => this.vimState.flash(ranges, options))
+ }
+
+ execute() {
+ const {newRanges, oldRanges} = this.mutateWithTrackChanges()
+
+ for (const selection of this.editor.getSelections()) {
+ selection.clear()
+ }
+
+ if (this.getConfig("setCursorToStartOfChangeOnUndoRedo")) {
+ const strategy = this.getConfig("setCursorToStartOfChangeOnUndoRedoStrategy")
+ this.setCursorPosition({newRanges, oldRanges, strategy})
+ this.vimState.clearSelections()
+ }
+
+ if (this.getConfig("flashOnUndoRedo")) this.flashChanges({newRanges, oldRanges})
+ this.activateMode("normal")
+ }
+
+ mutate() {
+ this.editor.undo()
+ }
+}
+Undo.register()
+
+class Redo extends Undo {
+ mutate() {
+ this.editor.redo()
+ }
+}
+Redo.register()
+
+// zc
+class FoldCurrentRow extends MiscCommand {
+ execute() {
+ for (const selection of this.editor.getSelections()) {
+ this.editor.foldBufferRow(this.getCursorPositionForSelection(selection).row)
+ }
+ }
+}
+FoldCurrentRow.register()
+
+// zo
+class UnfoldCurrentRow extends MiscCommand {
+ execute() {
+ for (const selection of this.editor.getSelections()) {
+ this.editor.unfoldBufferRow(this.getCursorPositionForSelection(selection).row)
+ }
+ }
+}
+UnfoldCurrentRow.register()
+
+// za
+class ToggleFold extends MiscCommand {
+ execute() {
+ this.editor.toggleFoldAtBufferRow(this.editor.getCursorBufferPosition().row)
+ }
+}
+ToggleFold.register()
+
+// Base of zC, zO, zA
+class FoldCurrentRowRecursivelyBase extends MiscCommand {
+ foldRecursively(row) {
+ const rowRanges = getFoldRowRangesContainedByFoldStartsAtRow(this.editor, row)
+ if (!rowRanges) return
+ const startRows = rowRanges.map(rowRange => rowRange[0])
+ for (const row of startRows.reverse()) {
+ if (!this.editor.isFoldedAtBufferRow(row)) {
+ this.editor.foldBufferRow(row)
+ }
+ }
+ }
+
+ unfoldRecursively(row) {
+ const rowRanges = getFoldRowRangesContainedByFoldStartsAtRow(this.editor, row)
+ if (!rowRanges) return
+ const startRows = rowRanges.map(rowRange => rowRange[0])
+ for (row of startRows) {
+ if (this.editor.isFoldedAtBufferRow(row)) {
+ this.editor.unfoldBufferRow(row)
+ }
+ }
+ }
+
+ foldRecursivelyForAllSelections() {
+ for (const selection of this.editor.getSelectionsOrderedByBufferPosition().reverse()) {
+ this.foldRecursively(this.getCursorPositionForSelection(selection).row)
+ }
+ }
+
+ unfoldRecursivelyForAllSelections() {
+ for (const selection of this.editor.getSelectionsOrderedByBufferPosition()) {
+ this.unfoldRecursively(this.getCursorPositionForSelection(selection).row)
+ }
+ }
+}
+FoldCurrentRowRecursivelyBase.register(false)
+
+// zC
+class FoldCurrentRowRecursively extends FoldCurrentRowRecursivelyBase {
+ execute() {
+ this.foldRecursivelyForAllSelections()
+ }
+}
+FoldCurrentRowRecursively.register()
+
+// zO
+class UnfoldCurrentRowRecursively extends FoldCurrentRowRecursivelyBase {
+ execute() {
+ this.unfoldRecursivelyForAllSelections()
+ }
+}
+UnfoldCurrentRowRecursively.register()
+
+// zA
+class ToggleFoldRecursively extends FoldCurrentRowRecursivelyBase {
+ execute() {
+ const {row} = this.getCursorPositionForSelection(this.editor.getLastSelection())
+ if (this.editor.isFoldedAtBufferRow(row)) {
+ this.unfoldRecursivelyForAllSelections()
+ } else {
+ this.foldRecursivelyForAllSelections()
+ }
+ }
+}
+ToggleFoldRecursively.register()
+
+// zR
+class UnfoldAll extends MiscCommand {
+ execute() {
+ this.editor.unfoldAll()
+ }
+}
+UnfoldAll.register()
+
+// zM
+class FoldAll extends MiscCommand {
+ execute() {
+ const {allFold} = getFoldInfoByKind(this.editor)
+ if (!allFold) return
+
+ this.editor.unfoldAll()
+ for (const {indent, startRow, endRow} of allFold.rowRangesWithIndent) {
+ if (indent <= this.getConfig("maxFoldableIndentLevel")) {
+ this.editor.foldBufferRowRange(startRow, endRow)
+ }
+ }
+ }
+}
+FoldAll.register()
+
+// zr
+class UnfoldNextIndentLevel extends MiscCommand {
+ execute() {
+ const {folded} = getFoldInfoByKind(this.editor)
+ if (!folded) return
+ const {minIndent, rowRangesWithIndent} = folded
+ const count = limitNumber(this.getCount() - 1, {min: 0})
+ const targetIndents = getList(minIndent, minIndent + count)
+ for (const {indent, startRow} of rowRangesWithIndent) {
+ if (targetIndents.includes(indent)) {
+ this.editor.unfoldBufferRow(startRow)
+ }
+ }
+ }
+}
+UnfoldNextIndentLevel.register()
+
+// zm
+class FoldNextIndentLevel extends MiscCommand {
+ execute() {
+ const {unfolded, allFold} = getFoldInfoByKind(this.editor)
+ if (!unfolded) return
+ // FIXME: Why I need unfoldAll()? Why can't I just fold non-folded-fold only?
+ // Unless unfoldAll() here, @editor.unfoldAll() delete foldMarker but fail
+ // to render unfolded rows correctly.
+ // I believe this is bug of text-buffer's markerLayer which assume folds are
+ // created **in-order** from top-row to bottom-row.
+ this.editor.unfoldAll()
+
+ const maxFoldable = this.getConfig("maxFoldableIndentLevel")
+ let fromLevel = Math.min(unfolded.maxIndent, maxFoldable)
+ const count = limitNumber(this.getCount() - 1, {min: 0})
+ fromLevel = limitNumber(fromLevel - count, {min: 0})
+ const targetIndents = getList(fromLevel, maxFoldable)
+ for (const {indent, startRow, endRow} of allFold.rowRangesWithIndent) {
+ if (targetIndents.includes(indent)) {
+ this.editor.foldBufferRowRange(startRow, endRow)
+ }
+ }
+ }
+}
+FoldNextIndentLevel.register()
+
+class ReplaceModeBackspace extends MiscCommand {
+ static commandScope = "atom-text-editor.vim-mode-plus.insert-mode.replace"
+
+ execute() {
+ for (const selection of this.editor.getSelections()) {
+ // char might be empty.
+ const char = this.vimState.modeManager.getReplacedCharForSelection(selection)
+ if (char != null) {
+ selection.selectLeft()
+ if (!selection.insertText(char).isEmpty()) selection.cursor.moveLeft()
+ }
+ }
+ }
+}
+ReplaceModeBackspace.register()
+
+class ScrollWithoutChangingCursorPosition extends MiscCommand {
+ scrolloff = 2 // atom default. Better to use editor.getVerticalScrollMargin()?
+ cursorPixel = null
+
+ getFirstVisibleScreenRow() {
+ return this.editorElement.getFirstVisibleScreenRow()
+ }
+
+ getLastVisibleScreenRow() {
+ return this.editorElement.getLastVisibleScreenRow()
+ }
+
+ getLastScreenRow() {
+ return this.editor.getLastScreenRow()
+ }
+
+ getCursorPixel() {
+ const point = this.editor.getCursorScreenPosition()
+ return this.editorElement.pixelPositionForScreenPosition(point)
+ }
+}
+ScrollWithoutChangingCursorPosition.register(false)
+
+// ctrl-e scroll lines downwards
+class ScrollDown extends ScrollWithoutChangingCursorPosition {
+ execute() {
+ const count = this.getCount()
+ const oldFirstRow = this.editor.getFirstVisibleScreenRow()
+ this.editor.setFirstVisibleScreenRow(oldFirstRow + count)
+ const newFirstRow = this.editor.getFirstVisibleScreenRow()
+
+ const offset = 2
+ const {row, column} = this.editor.getCursorScreenPosition()
+ if (row < newFirstRow + offset) {
+ const newPoint = [row + count, column]
+ this.editor.setCursorScreenPosition(newPoint, {autoscroll: false})
+ }
+ }
+}
+ScrollDown.register()
+
+// ctrl-y scroll lines upwards
+class ScrollUp extends ScrollWithoutChangingCursorPosition {
+ execute() {
+ const count = this.getCount()
+ const oldFirstRow = this.editor.getFirstVisibleScreenRow()
+ this.editor.setFirstVisibleScreenRow(oldFirstRow - count)
+ const newLastRow = this.editor.getLastVisibleScreenRow()
+
+ const offset = 2
+ const {row, column} = this.editor.getCursorScreenPosition()
+ if (row >= newLastRow - offset) {
+ const newPoint = [row - count, column]
+ this.editor.setCursorScreenPosition(newPoint, {autoscroll: false})
+ }
+ }
+}
+ScrollUp.register()
+
+// ScrollWithoutChangingCursorPosition without Cursor Position change.
+// -------------------------
+class ScrollCursor extends ScrollWithoutChangingCursorPosition {
+ moveToFirstCharacterOfLine = true
+
+ execute() {
+ if (this.moveToFirstCharacterOfLine) this.editor.moveToFirstCharacterOfLine()
+ if (this.isScrollable()) this.editorElement.setScrollTop(this.getScrollTop())
+ }
+
+ getOffSetPixelHeight(lineDelta = 0) {
+ return this.editor.getLineHeightInPixels() * (this.scrolloff + lineDelta)
+ }
+}
+ScrollCursor.register(false)
+
+// z enter
+class ScrollCursorToTop extends ScrollCursor {
+ isScrollable() {
+ return this.getLastVisibleScreenRow() !== this.getLastScreenRow()
+ }
+
+ getScrollTop() {
+ return this.getCursorPixel().top - this.getOffSetPixelHeight()
+ }
+}
+ScrollCursorToTop.register()
+
+// zt
+class ScrollCursorToTopLeave extends ScrollCursorToTop {
+ moveToFirstCharacterOfLine = false
+}
+ScrollCursorToTopLeave.register()
+
+// z-
+class ScrollCursorToBottom extends ScrollCursor {
+ isScrollable() {
+ return this.getFirstVisibleScreenRow() !== 0
+ }
+
+ getScrollTop() {
+ return this.getCursorPixel().top - (this.editorElement.getHeight() - this.getOffSetPixelHeight(1))
+ }
+}
+ScrollCursorToBottom.register()
+
+// zb
+class ScrollCursorToBottomLeave extends ScrollCursorToBottom {
+ moveToFirstCharacterOfLine = false
+}
+ScrollCursorToBottomLeave.register()
+
+// z.
+class ScrollCursorToMiddle extends ScrollCursor {
+ isScrollable() {
+ return true
+ }
+
+ getScrollTop() {
+ return this.getCursorPixel().top - this.editorElement.getHeight() / 2
+ }
+}
+ScrollCursorToMiddle.register()
+
+// zz
+class ScrollCursorToMiddleLeave extends ScrollCursorToMiddle {
+ moveToFirstCharacterOfLine = false
+}
+ScrollCursorToMiddleLeave.register()
+
+// Horizontal ScrollWithoutChangingCursorPosition
+// -------------------------
+// zs
+class ScrollCursorToLeft extends ScrollWithoutChangingCursorPosition {
+ execute() {
+ this.editorElement.setScrollLeft(this.getCursorPixel().left)
+ }
+}
+ScrollCursorToLeft.register()
+
+// ze
+class ScrollCursorToRight extends ScrollCursorToLeft {
+ execute() {
+ this.editorElement.setScrollRight(this.getCursorPixel().left)
+ }
+}
+ScrollCursorToRight.register()
+
+// insert-mode specific commands
+// -------------------------
+class InsertMode extends MiscCommand {}
+InsertMode.commandScope = "atom-text-editor.vim-mode-plus.insert-mode"
+
+class ActivateNormalModeOnce extends InsertMode {
+ execute() {
+ const cursorsToMoveRight = this.editor.getCursors().filter(cursor => !cursor.isAtBeginningOfLine())
+ this.vimState.activate("normal")
+ for (const cursor of cursorsToMoveRight) {
+ moveCursorRight(cursor)
+ }
+
+ let disposable = atom.commands.onDidDispatch(event => {
+ if (event.type === this.getCommandName()) return
+
+ disposable.dispose()
+ disposable = null
+ this.vimState.activate("insert")
+ })
+ }
+}
+ActivateNormalModeOnce.register()
+
+class InsertRegister extends InsertMode {
+ requireInput = true
+ initialize() {
+ this.readChar()
+ return super.initialize()
+ }
+
+ execute() {
+ this.editor.transact(() => {
+ for (const selection of this.editor.getSelections()) {
+ const text = this.vimState.register.getText(this.input, selection)
+ selection.insertText(text)
+ }
+ })
+ }
+}
+InsertRegister.register()
+
+class InsertLastInserted extends InsertMode {
+ execute() {
+ const text = this.vimState.register.getText(".")
+ this.editor.insertText(text)
+ }
+}
+InsertLastInserted.register()
+
+class CopyFromLineAbove extends InsertMode {
+ rowDelta = -1
+
+ execute() {
+ const translation = [this.rowDelta, 0]
+ this.editor.transact(() => {
+ for (let selection of this.editor.getSelections()) {
+ const point = selection.cursor.getBufferPosition().translate(translation)
+ if (point.row < 0) continue
+
+ const range = Range.fromPointWithDelta(point, 0, 1)
+ const text = this.editor.getTextInBufferRange(range)
+ if (text) selection.insertText(text)
+ }
+ })
+ }
+}
+CopyFromLineAbove.register()
+
+class CopyFromLineBelow extends CopyFromLineAbove {
+ rowDelta = +1
+}
+CopyFromLineBelow.register()
+
+class NextTab extends MiscCommand {
+ defaultCount = 0
+
+ execute() {
+ const count = this.getCount()
+ const pane = atom.workspace.paneForItem(this.editor)
+
+ if (count) pane.activateItemAtIndex(count - 1)
+ else pane.activateNextItem()
+ }
+}
+NextTab.register()
+
+class PreviousTab extends MiscCommand {
+ execute() {
+ atom.workspace.paneForItem(this.editor).activatePreviousItem()
+ }
+}
+PreviousTab.register()
diff --git a/lib/motion-search.coffee b/lib/motion-search.coffee
deleted file mode 100644
index fa69ca9e1..000000000
--- a/lib/motion-search.coffee
+++ /dev/null
@@ -1,232 +0,0 @@
-_ = require 'underscore-plus'
-
-{saveEditorState, getNonWordCharactersForCursor, searchByProjectFind} = require './utils'
-SearchModel = require './search-model'
-Motion = require('./base').getClass('Motion')
-
-class SearchBase extends Motion
- @extend(false)
- jump: true
- backwards: false
- useRegexp: true
- caseSensitivityKind: null
- landingPoint: null # ['start' or 'end']
- defaultLandingPoint: 'start' # ['start' or 'end']
- relativeIndex: null
- updatelastSearchPattern: true
-
- isBackwards: ->
- @backwards
-
- isIncrementalSearch: ->
- @instanceof('Search') and not @repeated and @getConfig('incrementalSearch')
-
- initialize: ->
- super
- @onDidFinishOperation =>
- @finish()
-
- getCount: ->
- count = super
- if @isBackwards()
- -count
- else
- count
-
- finish: ->
- if @isIncrementalSearch() and @getConfig('showHoverSearchCounter')
- @vimState.hoverSearchCounter.reset()
- @relativeIndex = null
- @searchModel?.destroy()
- @searchModel = null
-
- getLandingPoint: ->
- @landingPoint ?= @defaultLandingPoint
-
- getPoint: (cursor) ->
- if @searchModel?
- @relativeIndex = @getCount() + @searchModel.getRelativeIndex()
- else
- @relativeIndex ?= @getCount()
-
- if range = @search(cursor, @input, @relativeIndex)
- point = range[@getLandingPoint()]
-
- @searchModel.destroy()
- @searchModel = null
-
- point
-
- moveCursor: (cursor) ->
- input = @input
- return unless input
-
- if point = @getPoint(cursor)
- cursor.setBufferPosition(point, autoscroll: false)
-
- unless @repeated
- @globalState.set('currentSearch', this)
- @vimState.searchHistory.save(input)
-
- if @updatelastSearchPattern
- @globalState.set('lastSearchPattern', @getPattern(input))
-
- getSearchModel: ->
- @searchModel ?= new SearchModel(@vimState, incrementalSearch: @isIncrementalSearch())
-
- search: (cursor, input, relativeIndex) ->
- searchModel = @getSearchModel()
- if input
- fromPoint = @getBufferPositionForCursor(cursor)
- return searchModel.search(fromPoint, @getPattern(input), relativeIndex)
- else
- @vimState.hoverSearchCounter.reset()
- searchModel.clearMarkers()
-
-# /, ?
-# -------------------------
-class Search extends SearchBase
- @extend()
- caseSensitivityKind: "Search"
- requireInput: true
-
- initialize: ->
- super
- return if @isComplete() # When repeated, no need to get user input
-
- if @isIncrementalSearch()
- @restoreEditorState = saveEditorState(@editor)
- @onDidCommandSearch(@handleCommandEvent.bind(this))
-
- @onDidConfirmSearch(@handleConfirmSearch.bind(this))
- @onDidCancelSearch(@handleCancelSearch.bind(this))
- @onDidChangeSearch(@handleChangeSearch.bind(this))
-
- @focusSearchInputEditor()
-
- focusSearchInputEditor: ->
- classList = []
- classList.push('backwards') if @backwards
- @vimState.searchInput.focus({classList})
-
- handleCommandEvent: (commandEvent) ->
- return unless commandEvent.input
- switch commandEvent.name
- when 'visit'
- {direction} = commandEvent
- if @isBackwards() and @getConfig('incrementalSearchVisitDirection') is 'relative'
- direction = switch direction
- when 'next' then 'prev'
- when 'prev' then 'next'
-
- switch direction
- when 'next' then @getSearchModel().visit(+1)
- when 'prev' then @getSearchModel().visit(-1)
-
- when 'occurrence'
- {operation, input} = commandEvent
- @vimState.occurrenceManager.addPattern(@getPattern(input), reset: operation?)
- @vimState.occurrenceManager.saveLastPattern()
-
- @vimState.searchHistory.save(input)
- @vimState.searchInput.cancel()
-
- @vimState.operationStack.run(operation) if operation?
-
- when 'project-find'
- {input} = commandEvent
- @vimState.searchHistory.save(input)
- @vimState.searchInput.cancel()
- searchByProjectFind(@editor, input)
-
- handleCancelSearch: ->
- @vimState.resetNormalMode() unless @mode in ['visual', 'insert']
- @restoreEditorState?()
- @vimState.reset()
- @finish()
-
- isSearchRepeatCharacter: (char) ->
- if @isIncrementalSearch()
- char is ''
- else
- searchChar = if @isBackwards() then '?' else '/'
- char in ['', searchChar]
-
- handleConfirmSearch: ({@input, @landingPoint}) =>
- if @isSearchRepeatCharacter(@input)
- @input = @vimState.searchHistory.get('prev')
- atom.beep() unless @input
- @processOperation()
-
- handleChangeSearch: (input) ->
- # If input starts with space, remove first space and disable useRegexp.
- if input.startsWith(' ')
- input = input.replace(/^ /, '')
- @useRegexp = false
- @vimState.searchInput.updateOptionSettings({@useRegexp})
-
- if @isIncrementalSearch()
- @search(@editor.getLastCursor(), input, @getCount())
-
- getPattern: (term) ->
- modifiers = if @isCaseSensitive(term) then 'g' else 'gi'
- # FIXME this prevent search \\c itself.
- # DONT thinklessly mimic pure Vim. Instead, provide ignorecase button and shortcut.
- if term.indexOf('\\c') >= 0
- term = term.replace('\\c', '')
- modifiers += 'i' unless 'i' in modifiers
-
- if @useRegexp
- try
- return new RegExp(term, modifiers)
- catch
- null
-
- new RegExp(_.escapeRegExp(term), modifiers)
-
-class SearchBackwards extends Search
- @extend()
- backwards: true
-
-# *, #
-# -------------------------
-class SearchCurrentWord extends SearchBase
- @extend()
- caseSensitivityKind: "SearchCurrentWord"
-
- moveCursor: (cursor) ->
- @input ?= (
- wordRange = @getCurrentWordBufferRange()
- if wordRange?
- @editor.setCursorBufferPosition(wordRange.start)
- @editor.getTextInBufferRange(wordRange)
- else
- ''
- )
- super
-
- getPattern: (term) ->
- modifiers = if @isCaseSensitive(term) then 'g' else 'gi'
- pattern = _.escapeRegExp(term)
- if /\W/.test(term)
- new RegExp("#{pattern}\\b", modifiers)
- else
- new RegExp("\\b#{pattern}\\b", modifiers)
-
- getCurrentWordBufferRange: ->
- cursor = @editor.getLastCursor()
- point = cursor.getBufferPosition()
-
- nonWordCharacters = getNonWordCharactersForCursor(cursor)
- wordRegex = new RegExp("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+", 'g')
-
- found = null
- @scanForward wordRegex, {from: [point.row, 0], allowNextLine: false}, ({range, stop}) ->
- if range.end.isGreaterThan(point)
- found = range
- stop()
- found
-
-class SearchCurrentWordBackwards extends SearchCurrentWord
- @extend()
- backwards: true
diff --git a/lib/motion-search.js b/lib/motion-search.js
new file mode 100644
index 000000000..d7a8af55f
--- /dev/null
+++ b/lib/motion-search.js
@@ -0,0 +1,265 @@
+"use babel"
+
+const _ = require("underscore-plus")
+
+const {saveEditorState, getNonWordCharactersForCursor, searchByProjectFind} = require("./utils")
+const SearchModel = require("./search-model")
+const Motion = require("./base").getClass("Motion")
+
+class SearchBase extends Motion {
+ jump = true
+ backwards = false
+ useRegexp = true
+ caseSensitivityKind = null
+ landingPoint = null // ['start' or 'end']
+ defaultLandingPoint = "start" // ['start' or 'end']
+ relativeIndex = null
+ updatelastSearchPattern = true
+
+ isBackwards() {
+ return this.backwards
+ }
+
+ resetState() {
+ super.resetState()
+ this.relativeIndex = null
+ }
+
+ isIncrementalSearch() {
+ return this.instanceof("Search") && !this.repeated && this.getConfig("incrementalSearch")
+ }
+
+ initialize() {
+ this.onDidFinishOperation(() => this.finish())
+ return super.initialize()
+ }
+
+ getCount(...args) {
+ return super.getCount(...args) * (this.isBackwards() ? -1 : 1)
+ }
+
+ finish() {
+ if (this.isIncrementalSearch() && this.getConfig("showHoverSearchCounter")) {
+ this.vimState.hoverSearchCounter.reset()
+ }
+ if (this.searchModel) this.searchModel.destroy()
+
+ this.relativeIndex = null
+ this.searchModel = null
+ }
+
+ getLandingPoint() {
+ if (!this.landingPoint) this.landingPoint = this.defaultLandingPoint
+ return this.landingPoint
+ }
+
+ getPoint(cursor) {
+ if (this.searchModel) {
+ this.relativeIndex = this.getCount() + this.searchModel.getRelativeIndex()
+ } else if (this.relativeIndex == null) {
+ this.relativeIndex = this.getCount()
+ }
+
+ const range = this.search(cursor, this.input, this.relativeIndex)
+
+ this.searchModel.destroy()
+ this.searchModel = null
+
+ if (range) return range[this.getLandingPoint()]
+ }
+
+ moveCursor(cursor) {
+ if (!this.input) return
+ const point = this.getPoint(cursor)
+
+ if (point) cursor.setBufferPosition(point, {autoscroll: false})
+
+ if (!this.repeated) {
+ this.globalState.set("currentSearch", this)
+ this.vimState.searchHistory.save(this.input)
+ }
+
+ if (this.updatelastSearchPattern) {
+ this.globalState.set("lastSearchPattern", this.getPattern(this.input))
+ }
+ }
+
+ getSearchModel() {
+ if (!this.searchModel) {
+ this.searchModel = new SearchModel(this.vimState, {incrementalSearch: this.isIncrementalSearch()})
+ }
+ return this.searchModel
+ }
+
+ search(cursor, input, relativeIndex) {
+ const searchModel = this.getSearchModel()
+ if (input) {
+ const fromPoint = this.getBufferPositionForCursor(cursor)
+ return searchModel.search(fromPoint, this.getPattern(input), relativeIndex)
+ }
+ this.vimState.hoverSearchCounter.reset()
+ searchModel.clearMarkers()
+ }
+}
+SearchBase.register(false)
+
+// /, ?
+// -------------------------
+class Search extends SearchBase {
+ caseSensitivityKind = "Search"
+ requireInput = true
+
+ initialize() {
+ if (!this.isComplete()) {
+ if (this.isIncrementalSearch()) {
+ this.restoreEditorState = saveEditorState(this.editor)
+ this.onDidCommandSearch(this.handleCommandEvent.bind(this))
+ }
+
+ this.onDidConfirmSearch(this.handleConfirmSearch.bind(this))
+ this.onDidCancelSearch(this.handleCancelSearch.bind(this))
+ this.onDidChangeSearch(this.handleChangeSearch.bind(this))
+
+ this.focusSearchInputEditor()
+ }
+
+ return super.initialize()
+ }
+
+ focusSearchInputEditor() {
+ const classList = this.isBackwards() ? ["backwards"] : []
+ this.vimState.searchInput.focus({classList})
+ }
+
+ handleCommandEvent(event) {
+ if (!event.input) return
+
+ if (event.name === "visit") {
+ let {direction} = event
+ if (this.isBackwards() && this.getConfig("incrementalSearchVisitDirection") === "relative") {
+ direction = direction === "next" ? "prev" : "next"
+ }
+ this.getSearchModel().visit(direction === "next" ? +1 : -1)
+ } else if (event.name === "occurrence") {
+ const {operation, input} = event
+ this.occurrenceManager.addPattern(this.getPattern(input), {reset: operation != null})
+ this.occurrenceManager.saveLastPattern()
+
+ this.vimState.searchHistory.save(input)
+ this.vimState.searchInput.cancel()
+ if (operation != null) this.vimState.operationStack.run(operation)
+ } else if (event.name === "project-find") {
+ this.vimState.searchHistory.save(event.input)
+ this.vimState.searchInput.cancel()
+ searchByProjectFind(this.editor, event.input)
+ }
+ }
+
+ handleCancelSearch() {
+ if (!["visual", "insert"].includes(this.mode)) this.vimState.resetNormalMode()
+
+ if (this.restoreEditorState) this.restoreEditorState()
+ this.vimState.reset()
+ this.finish()
+ }
+
+ isSearchRepeatCharacter(char) {
+ return this.isIncrementalSearch() ? char === "" : ["", this.isBackwards() ? "?" : "/"].includes(char) // empty confirm or invoking-char
+ }
+
+ handleConfirmSearch({input, landingPoint}) {
+ this.input = input
+ this.landingPoint = landingPoint
+ if (this.isSearchRepeatCharacter(this.input)) {
+ this.input = this.vimState.searchHistory.get("prev")
+ if (!this.input) atom.beep()
+ }
+ this.processOperation()
+ }
+
+ handleChangeSearch(input) {
+ // If input starts with space, remove first space and disable useRegexp.
+ if (input.startsWith(" ")) {
+ // FIXME: Sould I remove this unknown hack and implement visible button to togle regexp?
+ input = input.replace(/^ /, "")
+ this.useRegexp = false
+ }
+ this.vimState.searchInput.updateOptionSettings({useRegexp: this.useRegexp})
+
+ if (this.isIncrementalSearch()) {
+ this.search(this.editor.getLastCursor(), input, this.getCount())
+ }
+ }
+
+ getPattern(term) {
+ let modifiers = this.isCaseSensitive(term) ? "g" : "gi"
+ // FIXME this prevent search \\c itself.
+ // DONT thinklessly mimic pure Vim. Instead, provide ignorecase button and shortcut.
+ if (term.indexOf("\\c") >= 0) {
+ term = term.replace("\\c", "")
+ if (!modifiers.includes("i")) modifiers += "i"
+ }
+
+ if (this.useRegexp) {
+ try {
+ return new RegExp(term, modifiers)
+ } catch (error) {}
+ }
+ return new RegExp(_.escapeRegExp(term), modifiers)
+ }
+}
+Search.register()
+
+class SearchBackwards extends Search {
+ backwards = true
+}
+SearchBackwards.register()
+
+// *, #
+// -------------------------
+class SearchCurrentWord extends SearchBase {
+ caseSensitivityKind = "SearchCurrentWord"
+
+ moveCursor(cursor) {
+ if (this.input == null) {
+ const wordRange = this.getCurrentWordBufferRange()
+ if (wordRange) {
+ this.editor.setCursorBufferPosition(wordRange.start)
+ this.input = this.editor.getTextInBufferRange(wordRange)
+ } else {
+ this.input = ""
+ }
+ }
+
+ super.moveCursor(cursor)
+ }
+
+ getPattern(term) {
+ const escaped = _.escapeRegExp(term)
+ const source = /\W/.test(term) ? `${escaped}\\b` : `\\b${escaped}\\b`
+ return new RegExp(source, this.isCaseSensitive(term) ? "g" : "gi")
+ }
+
+ getCurrentWordBufferRange() {
+ const cursor = this.editor.getLastCursor()
+ const point = cursor.getBufferPosition()
+
+ const nonWordCharacters = getNonWordCharactersForCursor(cursor)
+ const wordRegex = new RegExp(`[^\\s${_.escapeRegExp(nonWordCharacters)}]+`, "g")
+
+ let foundRange
+ this.scanForward(wordRegex, {from: [point.row, 0], allowNextLine: false}, ({range, stop}) => {
+ if (range.end.isGreaterThan(point)) {
+ foundRange = range
+ stop()
+ }
+ })
+ return foundRange
+ }
+}
+SearchCurrentWord.register()
+
+class SearchCurrentWordBackwards extends SearchCurrentWord {
+ backwards = true
+}
+SearchCurrentWordBackwards.register()
diff --git a/lib/motion.coffee b/lib/motion.coffee
deleted file mode 100644
index 0d316e2fa..000000000
--- a/lib/motion.coffee
+++ /dev/null
@@ -1,1265 +0,0 @@
-_ = require 'underscore-plus'
-{Point, Range} = require 'atom'
-
-{
- moveCursorLeft, moveCursorRight
- moveCursorUpScreen, moveCursorDownScreen
- pointIsAtVimEndOfFile
- getFirstVisibleScreenRow, getLastVisibleScreenRow
- getValidVimScreenRow, getValidVimBufferRow
- moveCursorToFirstCharacterAtRow
- sortRanges
- pointIsOnWhiteSpace
- moveCursorToNextNonWhitespace
- isEmptyRow
- getCodeFoldRowRanges
- getLargestFoldRangeContainsBufferRow
- isIncludeFunctionScopeForRow
- detectScopeStartPositionForScope
- getBufferRows
- getTextInScreenRange
- setBufferRow
- setBufferColumn
- limitNumber
- getIndex
- smartScrollToBufferPosition
- pointIsAtEndOfLineAtNonEmptyRow
- getEndOfLineForBufferRow
- findRangeInBufferRow
- saveEditorState
-} = require './utils'
-
-Base = require './base'
-
-class Motion extends Base
- @extend(false)
- @operationKind: 'motion'
- inclusive: false
- wise: 'characterwise'
- jump: false
- verticalMotion: false
- moveSucceeded: null
- moveSuccessOnLinewise: false
- selectSucceeded: false
-
- constructor: ->
- super
-
- if @mode is 'visual'
- @wise = @submode
- @initialize()
-
- isLinewise: -> @wise is 'linewise'
- isBlockwise: -> @wise is 'blockwise'
-
- forceWise: (wise) ->
- if wise is 'characterwise'
- if @wise is 'linewise'
- @inclusive = false
- else
- @inclusive = not @inclusive
- @wise = wise
-
- resetState: ->
- @selectSucceeded = false
-
- setBufferPositionSafely: (cursor, point) ->
- cursor.setBufferPosition(point) if point?
-
- setScreenPositionSafely: (cursor, point) ->
- cursor.setScreenPosition(point) if point?
-
- moveWithSaveJump: (cursor) ->
- if cursor.isLastCursor() and @jump
- cursorPosition = cursor.getBufferPosition()
-
- @moveCursor(cursor)
-
- if cursorPosition? and not cursorPosition.isEqual(cursor.getBufferPosition())
- @vimState.mark.set('`', cursorPosition)
- @vimState.mark.set("'", cursorPosition)
-
- execute: ->
- if @operator?
- @select()
- else
- @moveWithSaveJump(cursor) for cursor in @editor.getCursors()
- @editor.mergeCursors()
- @editor.mergeIntersectingSelections()
-
- # NOTE: Modify selection by modtion, selection is already "normalized" before this function is called.
- select: ->
- isOrWasVisual = @operator?.instanceof('SelectBase') or @is('CurrentSelection') # need to care was visual for `.` repeated.
- for selection in @editor.getSelections()
- selection.modifySelection =>
- @moveWithSaveJump(selection.cursor)
-
- selectSucceeded = @moveSucceeded ? not selection.isEmpty() or (@isLinewise() and @moveSuccessOnLinewise)
- @selectSucceeded or= selectSucceeded
-
- if isOrWasVisual or (selectSucceeded and (@inclusive or @isLinewise()))
- $selection = @swrap(selection)
- $selection.saveProperties(true) # save property of "already-normalized-selection"
- $selection.applyWise(@wise)
-
- @vimState.getLastBlockwiseSelection().autoscroll() if @wise is 'blockwise'
-
- setCursorBufferRow: (cursor, row, options) ->
- if @verticalMotion and not @getConfig('stayOnVerticalMotion')
- cursor.setBufferPosition(@getFirstCharacterPositionForBufferRow(row), options)
- else
- setBufferRow(cursor, row, options)
-
- # [NOTE]
- # Since this function checks cursor position change, a cursor position MUST be
- # updated IN callback(=fn)
- # Updating point only in callback is wrong-use of this funciton,
- # since it stops immediately because of not cursor position change.
- moveCursorCountTimes: (cursor, fn) ->
- oldPosition = cursor.getBufferPosition()
- @countTimes @getCount(), (state) ->
- fn(state)
- if (newPosition = cursor.getBufferPosition()).isEqual(oldPosition)
- state.stop()
- oldPosition = newPosition
-
- isCaseSensitive: (term) ->
- if @getConfig("useSmartcaseFor#{@caseSensitivityKind}")
- term.search(/[A-Z]/) isnt -1
- else
- not @getConfig("ignoreCaseFor#{@caseSensitivityKind}")
-
-# Used as operator's target in visual-mode.
-class CurrentSelection extends Motion
- @extend(false)
- selectionExtent: null
- blockwiseSelectionExtent: null
- inclusive: true
-
- initialize: ->
- super
- @pointInfoByCursor = new Map
-
- moveCursor: (cursor) ->
- if @mode is 'visual'
- if @isBlockwise()
- @blockwiseSelectionExtent = @swrap(cursor.selection).getBlockwiseSelectionExtent()
- else
- @selectionExtent = @editor.getSelectedBufferRange().getExtent()
- else
- # `.` repeat case
- point = cursor.getBufferPosition()
-
- if @blockwiseSelectionExtent?
- cursor.setBufferPosition(point.translate(@blockwiseSelectionExtent))
- else
- cursor.setBufferPosition(point.traverse(@selectionExtent))
-
- select: ->
- if @mode is 'visual'
- super
- else
- for cursor in @editor.getCursors() when pointInfo = @pointInfoByCursor.get(cursor)
- {cursorPosition, startOfSelection} = pointInfo
- if cursorPosition.isEqual(cursor.getBufferPosition())
- cursor.setBufferPosition(startOfSelection)
- super
-
- # * Purpose of pointInfoByCursor? see #235 for detail.
- # When stayOnTransformString is enabled, cursor pos is not set on start of
- # of selected range.
- # But I want following behavior, so need to preserve position info.
- # 1. `vj>.` -> indent same two rows regardless of current cursor's row.
- # 2. `vj>j.` -> indent two rows from cursor's row.
- for cursor in @editor.getCursors()
- startOfSelection = cursor.selection.getBufferRange().start
- @onDidFinishOperation =>
- cursorPosition = cursor.getBufferPosition()
- @pointInfoByCursor.set(cursor, {startOfSelection, cursorPosition})
-
-class MoveLeft extends Motion
- @extend()
- moveCursor: (cursor) ->
- allowWrap = @getConfig('wrapLeftRightMotion')
- @moveCursorCountTimes cursor, ->
- moveCursorLeft(cursor, {allowWrap})
-
-class MoveRight extends Motion
- @extend()
- canWrapToNextLine: (cursor) ->
- if @isAsTargetExceptSelectInVisualMode() and not cursor.isAtEndOfLine()
- false
- else
- @getConfig('wrapLeftRightMotion')
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- cursorPosition = cursor.getBufferPosition()
- @editor.unfoldBufferRow(cursorPosition.row)
- allowWrap = @canWrapToNextLine(cursor)
- moveCursorRight(cursor)
- if cursor.isAtEndOfLine() and allowWrap and not pointIsAtVimEndOfFile(@editor, cursorPosition)
- moveCursorRight(cursor, {allowWrap})
-
-class MoveRightBufferColumn extends Motion
- @extend(false)
-
- moveCursor: (cursor) ->
- setBufferColumn(cursor, cursor.getBufferColumn() + @getCount())
-
-class MoveUp extends Motion
- @extend()
- wise: 'linewise'
- wrap: false
-
- getBufferRow: (row) ->
- row = @getNextRow(row)
- if @editor.isFoldedAtBufferRow(row)
- getLargestFoldRangeContainsBufferRow(@editor, row).start.row
- else
- row
-
- getNextRow: (row) ->
- min = 0
- if @wrap and row is min
- @getVimLastBufferRow()
- else
- limitNumber(row - 1, {min})
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- setBufferRow(cursor, @getBufferRow(cursor.getBufferRow()))
-
-class MoveUpWrap extends MoveUp
- @extend()
- wrap: true
-
-class MoveDown extends MoveUp
- @extend()
- wise: 'linewise'
- wrap: false
-
- getBufferRow: (row) ->
- if @editor.isFoldedAtBufferRow(row)
- row = getLargestFoldRangeContainsBufferRow(@editor, row).end.row
- @getNextRow(row)
-
- getNextRow: (row) ->
- max = @getVimLastBufferRow()
- if @wrap and row >= max
- 0
- else
- limitNumber(row + 1, {max})
-
-class MoveDownWrap extends MoveDown
- @extend()
- wrap: true
-
-class MoveUpScreen extends Motion
- @extend()
- wise: 'linewise'
- direction: 'up'
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, ->
- moveCursorUpScreen(cursor)
-
-class MoveDownScreen extends MoveUpScreen
- @extend()
- wise: 'linewise'
- direction: 'down'
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, ->
- moveCursorDownScreen(cursor)
-
-# Move down/up to Edge
-# -------------------------
-# See t9md/atom-vim-mode-plus#236
-# At least v1.7.0. bufferPosition and screenPosition cannot convert accurately
-# when row is folded.
-class MoveUpToEdge extends Motion
- @extend()
- wise: 'linewise'
- jump: true
- direction: 'up'
- @description: "Move cursor up to **edge** char at same-column"
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- @setScreenPositionSafely(cursor, @getPoint(cursor.getScreenPosition()))
-
- getPoint: (fromPoint) ->
- column = fromPoint.column
- for row in @getScanRows(fromPoint) when @isEdge(point = new Point(row, column))
- return point
-
- getScanRows: ({row}) ->
- validRow = getValidVimScreenRow.bind(null, @editor)
- switch @direction
- when 'up' then [validRow(row - 1)..0]
- when 'down' then [validRow(row + 1)..@getVimLastScreenRow()]
-
- isEdge: (point) ->
- if @isStoppablePoint(point)
- # If one of above/below point was not stoppable, it's Edge!
- above = point.translate([-1, 0])
- below = point.translate([+1, 0])
- (not @isStoppablePoint(above)) or (not @isStoppablePoint(below))
- else
- false
-
- isStoppablePoint: (point) ->
- if @isNonWhiteSpacePoint(point) or @isFirstRowOrLastRowAndStoppable(point)
- true
- else
- leftPoint = point.translate([0, -1])
- rightPoint = point.translate([0, +1])
- @isNonWhiteSpacePoint(leftPoint) and @isNonWhiteSpacePoint(rightPoint)
-
- isNonWhiteSpacePoint: (point) ->
- char = getTextInScreenRange(@editor, Range.fromPointWithDelta(point, 0, 1))
- char? and /\S/.test(char)
-
- isFirstRowOrLastRowAndStoppable: (point) ->
- # In normal-mode we adjust cursor by moving-left if cursor at EOL of non-blank row.
- # So explicitly guard to not answer it stoppable.
- if @isMode('normal') and pointIsAtEndOfLineAtNonEmptyRow(@editor, point)
- false
- else
- point.isEqual(@editor.clipScreenPosition(point)) and
- ((point.row is 0) or (point.row is @getVimLastScreenRow()))
-
-class MoveDownToEdge extends MoveUpToEdge
- @extend()
- @description: "Move cursor down to **edge** char at same-column"
- direction: 'down'
-
-# word
-# -------------------------
-class MoveToNextWord extends Motion
- @extend()
- wordRegex: null
-
- getPoint: (pattern, from) ->
- wordRange = null
- found = false
- vimEOF = @getVimEofBufferPosition(@editor)
-
- @scanForward pattern, {from}, ({range, matchText, stop}) ->
- wordRange = range
- # Ignore 'empty line' matches between '\r' and '\n'
- return if matchText is '' and range.start.column isnt 0
- if range.start.isGreaterThan(from)
- found = true
- stop()
-
- if found
- point = wordRange.start
- if pointIsAtEndOfLineAtNonEmptyRow(@editor, point) and not point.isEqual(vimEOF)
- point.traverse([1, 0])
- else
- point
- else
- wordRange?.end ? from
-
- # Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
- # on a non-blank. This is because "cw" is interpreted as change-word, and a
- # word does not include the following white space. {Vi: "cw" when on a blank
- # followed by other blanks changes only the first blank; this is probably a
- # bug, because "dw" deletes all the blanks}
- #
- # Another special case: When using the "w" motion in combination with an
- # operator and the last word moved over is at the end of a line, the end of
- # that word becomes the end of the operated text, not the first word in the
- # next line.
- moveCursor: (cursor) ->
- cursorPosition = cursor.getBufferPosition()
- return if pointIsAtVimEndOfFile(@editor, cursorPosition)
- wasOnWhiteSpace = pointIsOnWhiteSpace(@editor, cursorPosition)
-
- isAsTargetExceptSelectInVisualMode = @isAsTargetExceptSelectInVisualMode()
- @moveCursorCountTimes cursor, ({isFinal}) =>
- cursorPosition = cursor.getBufferPosition()
- if isEmptyRow(@editor, cursorPosition.row) and isAsTargetExceptSelectInVisualMode
- point = cursorPosition.traverse([1, 0])
- else
- pattern = @wordRegex ? cursor.wordRegExp()
- point = @getPoint(pattern, cursorPosition)
- if isFinal and isAsTargetExceptSelectInVisualMode
- if @operator.is('Change') and (not wasOnWhiteSpace)
- point = cursor.getEndOfCurrentWordBufferPosition({@wordRegex})
- else
- point = Point.min(point, getEndOfLineForBufferRow(@editor, cursorPosition.row))
- cursor.setBufferPosition(point)
-
-# b
-class MoveToPreviousWord extends Motion
- @extend()
- wordRegex: null
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- point = cursor.getBeginningOfCurrentWordBufferPosition({@wordRegex})
- cursor.setBufferPosition(point)
-
-class MoveToEndOfWord extends Motion
- @extend()
- wordRegex: null
- inclusive: true
-
- moveToNextEndOfWord: (cursor) ->
- moveCursorToNextNonWhitespace(cursor)
- point = cursor.getEndOfCurrentWordBufferPosition({@wordRegex}).translate([0, -1])
- point = Point.min(point, @getVimEofBufferPosition())
- cursor.setBufferPosition(point)
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- originalPoint = cursor.getBufferPosition()
- @moveToNextEndOfWord(cursor)
- if originalPoint.isEqual(cursor.getBufferPosition())
- # Retry from right column if cursor was already on EndOfWord
- cursor.moveRight()
- @moveToNextEndOfWord(cursor)
-
-# [TODO: Improve, accuracy]
-class MoveToPreviousEndOfWord extends MoveToPreviousWord
- @extend()
- inclusive: true
-
- moveCursor: (cursor) ->
- times = @getCount()
- wordRange = cursor.getCurrentWordBufferRange()
- cursorPosition = cursor.getBufferPosition()
-
- # if we're in the middle of a word then we need to move to its start
- if cursorPosition.isGreaterThan(wordRange.start) and cursorPosition.isLessThan(wordRange.end)
- times += 1
-
- for [1..times]
- point = cursor.getBeginningOfCurrentWordBufferPosition({@wordRegex})
- cursor.setBufferPosition(point)
-
- @moveToNextEndOfWord(cursor)
- if cursor.getBufferPosition().isGreaterThanOrEqual(cursorPosition)
- cursor.setBufferPosition([0, 0])
-
- moveToNextEndOfWord: (cursor) ->
- point = cursor.getEndOfCurrentWordBufferPosition({@wordRegex}).translate([0, -1])
- point = Point.min(point, @getVimEofBufferPosition())
- cursor.setBufferPosition(point)
-
-# Whole word
-# -------------------------
-class MoveToNextWholeWord extends MoveToNextWord
- @extend()
- wordRegex: /^$|\S+/g
-
-class MoveToPreviousWholeWord extends MoveToPreviousWord
- @extend()
- wordRegex: /^$|\S+/g
-
-class MoveToEndOfWholeWord extends MoveToEndOfWord
- @extend()
- wordRegex: /\S+/
-
-# [TODO: Improve, accuracy]
-class MoveToPreviousEndOfWholeWord extends MoveToPreviousEndOfWord
- @extend()
- wordRegex: /\S+/
-
-# Alphanumeric word [Experimental]
-# -------------------------
-class MoveToNextAlphanumericWord extends MoveToNextWord
- @extend()
- @description: "Move to next alphanumeric(`/\w+/`) word"
- wordRegex: /\w+/g
-
-class MoveToPreviousAlphanumericWord extends MoveToPreviousWord
- @extend()
- @description: "Move to previous alphanumeric(`/\w+/`) word"
- wordRegex: /\w+/
-
-class MoveToEndOfAlphanumericWord extends MoveToEndOfWord
- @extend()
- @description: "Move to end of alphanumeric(`/\w+/`) word"
- wordRegex: /\w+/
-
-# Alphanumeric word [Experimental]
-# -------------------------
-class MoveToNextSmartWord extends MoveToNextWord
- @extend()
- @description: "Move to next smart word (`/[\w-]+/`) word"
- wordRegex: /[\w-]+/g
-
-class MoveToPreviousSmartWord extends MoveToPreviousWord
- @extend()
- @description: "Move to previous smart word (`/[\w-]+/`) word"
- wordRegex: /[\w-]+/
-
-class MoveToEndOfSmartWord extends MoveToEndOfWord
- @extend()
- @description: "Move to end of smart word (`/[\w-]+/`) word"
- wordRegex: /[\w-]+/
-
-# Subword
-# -------------------------
-class MoveToNextSubword extends MoveToNextWord
- @extend()
- moveCursor: (cursor) ->
- @wordRegex = cursor.subwordRegExp()
- super
-
-class MoveToPreviousSubword extends MoveToPreviousWord
- @extend()
- moveCursor: (cursor) ->
- @wordRegex = cursor.subwordRegExp()
- super
-
-class MoveToEndOfSubword extends MoveToEndOfWord
- @extend()
- moveCursor: (cursor) ->
- @wordRegex = cursor.subwordRegExp()
- super
-
-# Sentence
-# -------------------------
-# Sentence is defined as below
-# - end with ['.', '!', '?']
-# - optionally followed by [')', ']', '"', "'"]
-# - followed by ['$', ' ', '\t']
-# - paragraph boundary is also sentence boundary
-# - section boundary is also sentence boundary(ignore)
-class MoveToNextSentence extends Motion
- @extend()
- jump: true
- sentenceRegex: ///(?:[\.!\?][\)\]"']*\s+)|(\n|\r\n)///g
- direction: 'next'
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- @setBufferPositionSafely(cursor, @getPoint(cursor.getBufferPosition()))
-
- getPoint: (fromPoint) ->
- if @direction is 'next'
- @getNextStartOfSentence(fromPoint)
- else if @direction is 'previous'
- @getPreviousStartOfSentence(fromPoint)
-
- isBlankRow: (row) ->
- @editor.isBufferRowBlank(row)
-
- getNextStartOfSentence: (from) ->
- foundPoint = null
- @scanForward @sentenceRegex, {from}, ({range, matchText, match, stop}) =>
- if match[1]?
- [startRow, endRow] = [range.start.row, range.end.row]
- return if @skipBlankRow and @isBlankRow(endRow)
- if @isBlankRow(startRow) isnt @isBlankRow(endRow)
- foundPoint = @getFirstCharacterPositionForBufferRow(endRow)
- else
- foundPoint = range.end
- stop() if foundPoint?
- foundPoint ? @getVimEofBufferPosition()
-
- getPreviousStartOfSentence: (from) ->
- foundPoint = null
- @scanBackward @sentenceRegex, {from}, ({range, match, stop, matchText}) =>
- if match[1]?
- [startRow, endRow] = [range.start.row, range.end.row]
- if not @isBlankRow(endRow) and @isBlankRow(startRow)
- point = @getFirstCharacterPositionForBufferRow(endRow)
- if point.isLessThan(from)
- foundPoint = point
- else
- return if @skipBlankRow
- foundPoint = @getFirstCharacterPositionForBufferRow(startRow)
- else
- if range.end.isLessThan(from)
- foundPoint = range.end
- stop() if foundPoint?
- foundPoint ? [0, 0]
-
-class MoveToPreviousSentence extends MoveToNextSentence
- @extend()
- direction: 'previous'
-
-class MoveToNextSentenceSkipBlankRow extends MoveToNextSentence
- @extend()
- skipBlankRow: true
-
-class MoveToPreviousSentenceSkipBlankRow extends MoveToPreviousSentence
- @extend()
- skipBlankRow: true
-
-# Paragraph
-# -------------------------
-class MoveToNextParagraph extends Motion
- @extend()
- jump: true
- direction: 'next'
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- @setBufferPositionSafely(cursor, @getPoint(cursor.getBufferPosition()))
-
- getPoint: (fromPoint) ->
- startRow = fromPoint.row
- wasAtNonBlankRow = not @editor.isBufferRowBlank(startRow)
- for row in getBufferRows(@editor, {startRow, @direction})
- if @editor.isBufferRowBlank(row)
- return new Point(row, 0) if wasAtNonBlankRow
- else
- wasAtNonBlankRow = true
-
- # fallback
- switch @direction
- when 'previous' then new Point(0, 0)
- when 'next' then @getVimEofBufferPosition()
-
-class MoveToPreviousParagraph extends MoveToNextParagraph
- @extend()
- direction: 'previous'
-
-# -------------------------
-# keymap: 0
-class MoveToBeginningOfLine extends Motion
- @extend()
-
- moveCursor: (cursor) ->
- setBufferColumn(cursor, 0)
-
-class MoveToColumn extends Motion
- @extend()
-
- moveCursor: (cursor) ->
- setBufferColumn(cursor, @getCount(-1))
-
-class MoveToLastCharacterOfLine extends Motion
- @extend()
-
- moveCursor: (cursor) ->
- row = getValidVimBufferRow(@editor, cursor.getBufferRow() + @getCount(-1))
- cursor.setBufferPosition([row, Infinity])
- cursor.goalColumn = Infinity
-
-class MoveToLastNonblankCharacterOfLineAndDown extends Motion
- @extend()
- inclusive: true
-
- moveCursor: (cursor) ->
- point = @getPoint(cursor.getBufferPosition())
- cursor.setBufferPosition(point)
-
- getPoint: ({row}) ->
- row = limitNumber(row + @getCount(-1), max: @getVimLastBufferRow())
- range = findRangeInBufferRow(@editor, /\S|^/, row, direction: 'backward')
- range?.start ? new Point(row, 0)
-
-# MoveToFirstCharacterOfLine faimily
-# ------------------------------------
-# ^
-class MoveToFirstCharacterOfLine extends Motion
- @extend()
- moveCursor: (cursor) ->
- point = @getFirstCharacterPositionForBufferRow(cursor.getBufferRow())
- @setBufferPositionSafely(cursor, point)
-
-class MoveToFirstCharacterOfLineUp extends MoveToFirstCharacterOfLine
- @extend()
- wise: 'linewise'
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, ->
- point = cursor.getBufferPosition()
- unless point.row is 0
- cursor.setBufferPosition(point.translate([-1, 0]))
- super
-
-class MoveToFirstCharacterOfLineDown extends MoveToFirstCharacterOfLine
- @extend()
- wise: 'linewise'
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- point = cursor.getBufferPosition()
- unless @getVimLastBufferRow() is point.row
- cursor.setBufferPosition(point.translate([+1, 0]))
- super
-
-class MoveToFirstCharacterOfLineAndDown extends MoveToFirstCharacterOfLineDown
- @extend()
- defaultCount: 0
- getCount: -> super - 1
-
-# keymap: g g
-class MoveToFirstLine extends Motion
- @extend()
- wise: 'linewise'
- jump: true
- verticalMotion: true
- moveSuccessOnLinewise: true
-
- moveCursor: (cursor) ->
- @setCursorBufferRow(cursor, getValidVimBufferRow(@editor, @getRow()))
- cursor.autoscroll(center: true)
-
- getRow: ->
- @getCount(-1)
-
-class MoveToScreenColumn extends Motion
- @extend(false)
- moveCursor: (cursor) ->
- allowOffScreenPosition = @getConfig("allowMoveToOffScreenColumnOnScreenLineMotion")
- point = @vimState.utils.getScreenPositionForScreenRow(@editor, cursor.getScreenRow(), @which, {allowOffScreenPosition})
- @setScreenPositionSafely(cursor, point)
-
-# keymap: g 0
-class MoveToBeginningOfScreenLine extends MoveToScreenColumn
- @extend()
- which: "beginning"
-
-# g ^: `move-to-first-character-of-screen-line`
-class MoveToFirstCharacterOfScreenLine extends MoveToScreenColumn
- @extend()
- which: "first-character"
-
-# keymap: g $
-class MoveToLastCharacterOfScreenLine extends MoveToScreenColumn
- @extend()
- which: "last-character"
-
-# keymap: G
-class MoveToLastLine extends MoveToFirstLine
- @extend()
- defaultCount: Infinity
-
-# keymap: N% e.g. 10%
-class MoveToLineByPercent extends MoveToFirstLine
- @extend()
-
- getRow: ->
- percent = limitNumber(@getCount(), max: 100)
- Math.floor((@editor.getLineCount() - 1) * (percent / 100))
-
-class MoveToRelativeLine extends Motion
- @extend(false)
- wise: 'linewise'
- moveSuccessOnLinewise: true
-
- moveCursor: (cursor) ->
- row = @getFoldEndRowForRow(cursor.getBufferRow())
-
- count = @getCount(-1)
- while (count > 0)
- row = @getFoldEndRowForRow(row + 1)
- count--
-
- setBufferRow(cursor, row)
-
-class MoveToRelativeLineMinimumOne extends MoveToRelativeLine
- @extend(false)
-
- getCount: ->
- limitNumber(super, min: 1)
-
-# Position cursor without scrolling., H, M, L
-# -------------------------
-# keymap: H
-class MoveToTopOfScreen extends Motion
- @extend()
- wise: 'linewise'
- jump: true
- scrolloff: 2
- defaultCount: 0
- verticalMotion: true
-
- moveCursor: (cursor) ->
- bufferRow = @editor.bufferRowForScreenRow(@getScreenRow())
- @setCursorBufferRow(cursor, bufferRow)
-
- getScrolloff: ->
- if @isAsTargetExceptSelectInVisualMode()
- 0
- else
- @scrolloff
-
- getScreenRow: ->
- firstRow = getFirstVisibleScreenRow(@editor)
- offset = @getScrolloff()
- offset = 0 if firstRow is 0
- offset = limitNumber(@getCount(-1), min: offset)
- firstRow + offset
-
-# keymap: M
-class MoveToMiddleOfScreen extends MoveToTopOfScreen
- @extend()
- getScreenRow: ->
- startRow = getFirstVisibleScreenRow(@editor)
- endRow = limitNumber(@editor.getLastVisibleScreenRow(), max: @getVimLastScreenRow())
- startRow + Math.floor((endRow - startRow) / 2)
-
-# keymap: L
-class MoveToBottomOfScreen extends MoveToTopOfScreen
- @extend()
- getScreenRow: ->
- # [FIXME]
- # At least Atom v1.6.0, there are two implementation of getLastVisibleScreenRow()
- # editor.getLastVisibleScreenRow() and editorElement.getLastVisibleScreenRow()
- # Those two methods return different value, editor's one is corrent.
- # So I intentionally use editor.getLastScreenRow here.
- vimLastScreenRow = @getVimLastScreenRow()
- row = limitNumber(@editor.getLastVisibleScreenRow(), max: vimLastScreenRow)
- offset = @getScrolloff() + 1
- offset = 0 if row is vimLastScreenRow
- offset = limitNumber(@getCount(-1), min: offset)
- row - offset
-
-# Scrolling
-# Half: ctrl-d, ctrl-u
-# Full: ctrl-f, ctrl-b
-# -------------------------
-# [FIXME] count behave differently from original Vim.
-class Scroll extends Motion
- @extend(false)
- verticalMotion: true
-
- isSmoothScrollEnabled: ->
- if Math.abs(@amountOfPage) is 1
- @getConfig('smoothScrollOnFullScrollMotion')
- else
- @getConfig('smoothScrollOnHalfScrollMotion')
-
- getSmoothScrollDuation: ->
- if Math.abs(@amountOfPage) is 1
- @getConfig('smoothScrollOnFullScrollMotionDuration')
- else
- @getConfig('smoothScrollOnHalfScrollMotionDuration')
-
- getPixelRectTopForSceenRow: (row) ->
- point = new Point(row, 0)
- @editor.element.pixelRectForScreenRange(new Range(point, point)).top
-
- smoothScroll: (fromRow, toRow, done) ->
- topPixelFrom = {top: @getPixelRectTopForSceenRow(fromRow)}
- topPixelTo = {top: @getPixelRectTopForSceenRow(toRow)}
- # [NOTE]
- # intentionally use `element.component.setScrollTop` instead of `element.setScrollTop`.
- # SInce element.setScrollTop will throw exception when element.component no longer exists.
- step = (newTop) =>
- if @editor.element.component?
- @editor.element.component.setScrollTop(newTop)
- @editor.element.component.updateSync()
-
- duration = @getSmoothScrollDuation()
- @vimState.requestScrollAnimation(topPixelFrom, topPixelTo, {duration, step, done})
-
- getAmountOfRows: ->
- Math.ceil(@amountOfPage * @editor.getRowsPerPage() * @getCount())
-
- getBufferRow: (cursor) ->
- screenRow = getValidVimScreenRow(@editor, cursor.getScreenRow() + @getAmountOfRows())
- @editor.bufferRowForScreenRow(screenRow)
-
- moveCursor: (cursor) ->
- bufferRow = @getBufferRow(cursor)
- @setCursorBufferRow(cursor, @getBufferRow(cursor), autoscroll: false)
-
- if cursor.isLastCursor()
- if @isSmoothScrollEnabled()
- @vimState.finishScrollAnimation()
-
- firstVisibileScreenRow = @editor.getFirstVisibleScreenRow()
- newFirstVisibileBufferRow = @editor.bufferRowForScreenRow(firstVisibileScreenRow + @getAmountOfRows())
- newFirstVisibileScreenRow = @editor.screenRowForBufferRow(newFirstVisibileBufferRow)
- done = =>
- @editor.setFirstVisibleScreenRow(newFirstVisibileScreenRow)
- # [FIXME] sometimes, scrollTop is not updated, calling this fix.
- # Investigate and find better approach then remove this workaround.
- @editor.element.component?.updateSync()
-
- if @isSmoothScrollEnabled()
- @smoothScroll(firstVisibileScreenRow, newFirstVisibileScreenRow, done)
- else
- done()
-
-
-# keymap: ctrl-f
-class ScrollFullScreenDown extends Scroll
- @extend(true)
- amountOfPage: +1
-
-# keymap: ctrl-b
-class ScrollFullScreenUp extends Scroll
- @extend()
- amountOfPage: -1
-
-# keymap: ctrl-d
-class ScrollHalfScreenDown extends Scroll
- @extend()
- amountOfPage: +1 / 2
-
-# keymap: ctrl-u
-class ScrollHalfScreenUp extends Scroll
- @extend()
- amountOfPage: -1 / 2
-
-# Find
-# -------------------------
-# keymap: f
-class Find extends Motion
- @extend()
- backwards: false
- inclusive: true
- offset: 0
- requireInput: true
- caseSensitivityKind: "Find"
-
- restoreEditorState: ->
- @_restoreEditorState?()
- @_restoreEditorState = null
-
- cancelOperation: ->
- @restoreEditorState()
- super
-
- initialize: ->
- super
-
- @repeatIfNecessary() if @getConfig("reuseFindForRepeatFind")
- return if @isComplete()
-
- charsMax = @getConfig("findCharsMax")
-
- if (charsMax > 1)
- @_restoreEditorState = saveEditorState(@editor)
-
- options =
- autoConfirmTimeout: @getConfig("findConfirmByTimeout")
- onConfirm: (@input) => if @input then @processOperation() else @cancelOperation()
- onChange: (@preConfirmedChars) => @highlightTextInCursorRows(@preConfirmedChars, "pre-confirm")
- onCancel: =>
- @vimState.highlightFind.clearMarkers()
- @cancelOperation()
- commands:
- "vim-mode-plus:find-next-pre-confirmed": => @findPreConfirmed(+1)
- "vim-mode-plus:find-previous-pre-confirmed": => @findPreConfirmed(-1)
-
- options ?= {}
- options.purpose = "find"
- options.charsMax = charsMax
-
- @focusInput(options)
-
- findPreConfirmed: (delta) ->
- if @preConfirmedChars and @getConfig("highlightFindChar")
- index = @highlightTextInCursorRows(@preConfirmedChars, "pre-confirm", @getCount(-1) + delta, true)
- @count = index + 1
-
- repeatIfNecessary: ->
- currentFind = @vimState.globalState.get("currentFind")
- isSequentialExecution = @vimState.operationStack.getLastCommandName() in ["Find", "FindBackwards", "Till", "TillBackwards"]
- if currentFind? and isSequentialExecution
- @input = currentFind.input
- @repeated = true
-
- isBackwards: ->
- @backwards
-
- execute: ->
- super
- decorationType = "post-confirm"
- decorationType += " long" if (@operator? and not @operator?.instanceof("SelectBase"))
- @editor.component.getNextUpdatePromise().then =>
- @highlightTextInCursorRows(@input, decorationType)
-
- return # Don't return Promise here. OperationStack treat Promise differently.
-
- getPoint: (fromPoint) ->
- scanRange = @editor.bufferRangeForBufferRow(fromPoint.row)
- points = []
- regex = @getRegex(@input)
- indexWantAccess = @getCount(-1)
-
- translation = new Point(0, if @isBackwards() then @offset else -@offset)
- fromPoint = fromPoint.translate(translation.negate()) if @repeated
-
- if @isBackwards()
- scanRange.start = Point.ZERO if @getConfig("findAcrossLines")
- @editor.backwardsScanInBufferRange regex, scanRange, ({range, stop}) ->
- if range.start.isLessThan(fromPoint)
- points.push(range.start)
- stop() if points.length > indexWantAccess
- else
- scanRange.end = @editor.getEofBufferPosition() if @getConfig("findAcrossLines")
- @editor.scanInBufferRange regex, scanRange, ({range, stop}) ->
- if range.start.isGreaterThan(fromPoint)
- points.push(range.start)
- stop() if points.length > indexWantAccess
-
- points[indexWantAccess]?.translate(translation)
-
- highlightTextInCursorRows: (text, decorationType, index = @getCount(-1), adjustIndex = false) ->
- return unless @getConfig("highlightFindChar")
- @vimState.highlightFind.highlightCursorRows(@getRegex(text), decorationType, @isBackwards(), @offset, index, adjustIndex)
-
- moveCursor: (cursor) ->
- point = @getPoint(cursor.getBufferPosition())
- if point?
- cursor.setBufferPosition(point)
- else
- @restoreEditorState()
-
- @globalState.set('currentFind', this) unless @repeated
-
- getRegex: (term) ->
- modifiers = if @isCaseSensitive(term) then 'g' else 'gi'
- new RegExp(_.escapeRegExp(term), modifiers)
-
-# keymap: F
-class FindBackwards extends Find
- @extend()
- inclusive: false
- backwards: true
-
-# keymap: t
-class Till extends Find
- @extend()
- offset: 1
-
- getPoint: ->
- @point = super
- @moveSucceeded = @point?
- return @point
-
-# keymap: T
-class TillBackwards extends Till
- @extend()
- inclusive: false
- backwards: true
-
-# Mark
-# -------------------------
-# keymap: `
-class MoveToMark extends Motion
- @extend()
- jump: true
- requireInput: true
- input: null # set when instatntiated via vimState::moveToMark()
-
- initialize: ->
- super
- @readChar() unless @isComplete()
-
- getPoint: ->
- @vimState.mark.get(@input)
-
- moveCursor: (cursor) ->
- if point = @getPoint()
- cursor.setBufferPosition(point)
- cursor.autoscroll(center: true)
-
-# keymap: '
-class MoveToMarkLine extends MoveToMark
- @extend()
- wise: 'linewise'
-
- getPoint: ->
- if point = super
- @getFirstCharacterPositionForBufferRow(point.row)
-
-# Fold
-# -------------------------
-class MoveToPreviousFoldStart extends Motion
- @extend()
- @description: "Move to previous fold start"
- wise: 'characterwise'
- which: 'start'
- direction: 'prev'
-
- initialize: ->
- super
- @rows = @getFoldRows(@which)
- @rows.reverse() if @direction is 'prev'
-
- getFoldRows: (which) ->
- index = if which is 'start' then 0 else 1
- rows = getCodeFoldRowRanges(@editor).map (rowRange) ->
- rowRange[index]
- _.sortBy(_.uniq(rows), (row) -> row)
-
- getScanRows: (cursor) ->
- cursorRow = cursor.getBufferRow()
- isValidRow = switch @direction
- when 'prev' then (row) -> row < cursorRow
- when 'next' then (row) -> row > cursorRow
- @rows.filter(isValidRow)
-
- detectRow: (cursor) ->
- @getScanRows(cursor)[0]
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- if (row = @detectRow(cursor))?
- moveCursorToFirstCharacterAtRow(cursor, row)
-
-class MoveToNextFoldStart extends MoveToPreviousFoldStart
- @extend()
- @description: "Move to next fold start"
- direction: 'next'
-
-class MoveToPreviousFoldStartWithSameIndent extends MoveToPreviousFoldStart
- @extend()
- @description: "Move to previous same-indented fold start"
- detectRow: (cursor) ->
- baseIndentLevel = @getIndentLevelForBufferRow(cursor.getBufferRow())
- for row in @getScanRows(cursor)
- if @getIndentLevelForBufferRow(row) is baseIndentLevel
- return row
- null
-
-class MoveToNextFoldStartWithSameIndent extends MoveToPreviousFoldStartWithSameIndent
- @extend()
- @description: "Move to next same-indented fold start"
- direction: 'next'
-
-class MoveToPreviousFoldEnd extends MoveToPreviousFoldStart
- @extend()
- @description: "Move to previous fold end"
- which: 'end'
-
-class MoveToNextFoldEnd extends MoveToPreviousFoldEnd
- @extend()
- @description: "Move to next fold end"
- direction: 'next'
-
-# -------------------------
-class MoveToPreviousFunction extends MoveToPreviousFoldStart
- @extend()
- @description: "Move to previous function"
- direction: 'prev'
- detectRow: (cursor) ->
- _.detect @getScanRows(cursor), (row) =>
- isIncludeFunctionScopeForRow(@editor, row)
-
-class MoveToNextFunction extends MoveToPreviousFunction
- @extend()
- @description: "Move to next function"
- direction: 'next'
-
-# Scope based
-# -------------------------
-class MoveToPositionByScope extends Motion
- @extend(false)
- direction: 'backward'
- scope: '.'
-
- getPoint: (fromPoint) ->
- detectScopeStartPositionForScope(@editor, fromPoint, @direction, @scope)
-
- moveCursor: (cursor) ->
- @moveCursorCountTimes cursor, =>
- @setBufferPositionSafely(cursor, @getPoint(cursor.getBufferPosition()))
-
-class MoveToPreviousString extends MoveToPositionByScope
- @extend()
- @description: "Move to previous string(searched by `string.begin` scope)"
- direction: 'backward'
- scope: 'string.begin'
-
-class MoveToNextString extends MoveToPreviousString
- @extend()
- @description: "Move to next string(searched by `string.begin` scope)"
- direction: 'forward'
-
-class MoveToPreviousNumber extends MoveToPositionByScope
- @extend()
- direction: 'backward'
- @description: "Move to previous number(searched by `constant.numeric` scope)"
- scope: 'constant.numeric'
-
-class MoveToNextNumber extends MoveToPreviousNumber
- @extend()
- @description: "Move to next number(searched by `constant.numeric` scope)"
- direction: 'forward'
-
-class MoveToNextOccurrence extends Motion
- @extend()
- # Ensure this command is available when has-occurrence
- @commandScope: 'atom-text-editor.vim-mode-plus.has-occurrence'
- jump: true
- direction: 'next'
-
- getRanges: ->
- @vimState.occurrenceManager.getMarkers().map (marker) ->
- marker.getBufferRange()
-
- execute: ->
- @ranges = @vimState.utils.sortRanges(@getRanges())
- super
-
- moveCursor: (cursor) ->
- range = @ranges[getIndex(@getIndex(cursor.getBufferPosition()), @ranges)]
- point = range.start
- cursor.setBufferPosition(point, autoscroll: false)
-
- if cursor.isLastCursor()
- @editor.unfoldBufferRow(point.row)
- smartScrollToBufferPosition(@editor, point)
-
- if @getConfig('flashOnMoveToOccurrence')
- @vimState.flash(range, type: 'search')
-
- getIndex: (fromPoint) ->
- index = null
- for range, i in @ranges when range.start.isGreaterThan(fromPoint)
- index = i
- break
- (index ? 0) + @getCount(-1)
-
-class MoveToPreviousOccurrence extends MoveToNextOccurrence
- @extend()
- direction: 'previous'
-
- getIndex: (fromPoint) ->
- index = null
- for range, i in @ranges by -1 when range.end.isLessThan(fromPoint)
- index = i
- break
- (index ? @ranges.length - 1) - @getCount(-1)
-
-# -------------------------
-# keymap: %
-class MoveToPair extends Motion
- @extend()
- inclusive: true
- jump: true
- member: ['Parenthesis', 'CurlyBracket', 'SquareBracket']
-
- moveCursor: (cursor) ->
- @setBufferPositionSafely(cursor, @getPoint(cursor))
-
- getPointForTag: (point) ->
- pairInfo = @new("ATag").getPairInfo(point)
- return null unless pairInfo?
- {openRange, closeRange} = pairInfo
- openRange = openRange.translate([0, +1], [0, -1])
- closeRange = closeRange.translate([0, +1], [0, -1])
- return closeRange.start if openRange.containsPoint(point) and (not point.isEqual(openRange.end))
- return openRange.start if closeRange.containsPoint(point) and (not point.isEqual(closeRange.end))
-
- getPoint: (cursor) ->
- cursorPosition = cursor.getBufferPosition()
- cursorRow = cursorPosition.row
- return point if point = @getPointForTag(cursorPosition)
-
- # AAnyPairAllowForwarding return forwarding range or enclosing range.
- range = @new("AAnyPairAllowForwarding", {@member}).getRange(cursor.selection)
- return null unless range?
- {start, end} = range
- if (start.row is cursorRow) and start.isGreaterThanOrEqual(cursorPosition)
- # Forwarding range found
- end.translate([0, -1])
- else if end.row is cursorPosition.row
- # Enclosing range was returned
- # We move to start( open-pair ) only when close-pair was at same row as cursor-row.
- start
diff --git a/lib/motion.js b/lib/motion.js
new file mode 100644
index 000000000..42b27ebc3
--- /dev/null
+++ b/lib/motion.js
@@ -0,0 +1,1506 @@
+"use babel"
+
+const _ = require("underscore-plus")
+const {Point, Range} = require("atom")
+
+const {
+ moveCursorLeft,
+ moveCursorRight,
+ moveCursorUpScreen,
+ moveCursorDownScreen,
+ pointIsAtVimEndOfFile,
+ getFirstVisibleScreenRow,
+ getLastVisibleScreenRow,
+ getValidVimScreenRow,
+ getValidVimBufferRow,
+ moveCursorToFirstCharacterAtRow,
+ sortRanges,
+ pointIsOnWhiteSpace,
+ moveCursorToNextNonWhitespace,
+ isEmptyRow,
+ getCodeFoldRowRanges,
+ getLargestFoldRangeContainsBufferRow,
+ isIncludeFunctionScopeForRow,
+ detectScopeStartPositionForScope,
+ getBufferRows,
+ getTextInScreenRange,
+ setBufferRow,
+ setBufferColumn,
+ limitNumber,
+ getIndex,
+ smartScrollToBufferPosition,
+ pointIsAtEndOfLineAtNonEmptyRow,
+ getEndOfLineForBufferRow,
+ findRangeInBufferRow,
+ saveEditorState,
+ getList,
+ getScreenPositionForScreenRow,
+} = require("./utils")
+
+const Base = require("./base")
+
+class Motion extends Base {
+ static operationKind = "motion"
+ inclusive = false
+ wise = "characterwise"
+ jump = false
+ verticalMotion = false
+ moveSucceeded = null
+ moveSuccessOnLinewise = false
+ selectSucceeded = false
+
+ isLinewise() {
+ return this.wise === "linewise"
+ }
+
+ isBlockwise() {
+ return this.wise === "blockwise"
+ }
+
+ forceWise(wise) {
+ if (wise === "characterwise") {
+ this.inclusive = this.wise === "linewise" ? false : !this.inclusive
+ }
+ this.wise = wise
+ }
+
+ resetState() {
+ this.selectSucceeded = false
+ }
+
+ setBufferPositionSafely(cursor, point) {
+ if (point) cursor.setBufferPosition(point)
+ }
+
+ setScreenPositionSafely(cursor, point) {
+ if (point) cursor.setScreenPosition(point)
+ }
+
+ moveWithSaveJump(cursor) {
+ let cursorPosition
+ if (cursor.isLastCursor() && this.jump) {
+ cursorPosition = cursor.getBufferPosition()
+ }
+
+ this.moveCursor(cursor)
+
+ if (cursorPosition && !cursorPosition.isEqual(cursor.getBufferPosition())) {
+ this.vimState.mark.set("`", cursorPosition)
+ this.vimState.mark.set("'", cursorPosition)
+ }
+ }
+
+ execute() {
+ if (this.operator) {
+ this.select()
+ } else {
+ for (const cursor of this.editor.getCursors()) {
+ this.moveWithSaveJump(cursor)
+ }
+ }
+ this.editor.mergeCursors()
+ this.editor.mergeIntersectingSelections()
+ }
+
+ // NOTE: Modify selection by modtion, selection is already "normalized" before this function is called.
+ select() {
+ // need to care was visual for `.` repeated.
+ const isOrWasVisual = (this.operator && this.operator.instanceof("SelectBase")) || this.is("CurrentSelection")
+
+ for (const selection of this.editor.getSelections()) {
+ selection.modifySelection(() => this.moveWithSaveJump(selection.cursor))
+
+ const selectSucceeded =
+ this.moveSucceeded != null
+ ? this.moveSucceeded
+ : !selection.isEmpty() || (this.isLinewise() && this.moveSuccessOnLinewise)
+ if (!this.selectSucceeded) this.selectSucceeded = selectSucceeded
+
+ if (isOrWasVisual || (selectSucceeded && (this.inclusive || this.isLinewise()))) {
+ const $selection = this.swrap(selection)
+ $selection.saveProperties(true) // save property of "already-normalized-selection"
+ $selection.applyWise(this.wise)
+ }
+ }
+
+ if (this.wise === "blockwise") {
+ this.vimState.getLastBlockwiseSelection().autoscroll()
+ }
+ }
+
+ setCursorBufferRow(cursor, row, options) {
+ if (this.verticalMotion && !this.getConfig("stayOnVerticalMotion")) {
+ cursor.setBufferPosition(this.getFirstCharacterPositionForBufferRow(row), options)
+ } else {
+ setBufferRow(cursor, row, options)
+ }
+ }
+
+ // [NOTE]
+ // Since this function checks cursor position change, a cursor position MUST be
+ // updated IN callback(=fn)
+ // Updating point only in callback is wrong-use of this funciton,
+ // since it stops immediately because of not cursor position change.
+ moveCursorCountTimes(cursor, fn) {
+ let oldPosition = cursor.getBufferPosition()
+ this.countTimes(this.getCount(), state => {
+ fn(state)
+ const newPosition = cursor.getBufferPosition()
+ if (newPosition.isEqual(oldPosition)) state.stop()
+ oldPosition = newPosition
+ })
+ }
+
+ isCaseSensitive(term) {
+ return this.getConfig(`useSmartcaseFor${this.caseSensitivityKind}`)
+ ? term.search(/[A-Z]/) !== -1
+ : !this.getConfig(`ignoreCaseFor${this.caseSensitivityKind}`)
+ }
+}
+Motion.register(false)
+
+// Used as operator's target in visual-mode.
+class CurrentSelection extends Motion {
+ selectionExtent = null
+ blockwiseSelectionExtent = null
+ inclusive = true
+
+ initialize() {
+ this.pointInfoByCursor = new Map()
+ return super.initialize()
+ }
+
+ moveCursor(cursor) {
+ if (this.mode === "visual") {
+ this.selectionExtent = this.isBlockwise()
+ ? this.swrap(cursor.selection).getBlockwiseSelectionExtent()
+ : this.editor.getSelectedBufferRange().getExtent()
+ } else {
+ // `.` repeat case
+ cursor.setBufferPosition(cursor.getBufferPosition().translate(this.selectionExtent))
+ }
+ }
+
+ select() {
+ if (this.mode === "visual") {
+ super.select()
+ } else {
+ for (const cursor of this.editor.getCursors()) {
+ const pointInfo = this.pointInfoByCursor.get(cursor)
+ if (pointInfo) {
+ const {cursorPosition, startOfSelection} = pointInfo
+ if (cursorPosition.isEqual(cursor.getBufferPosition())) {
+ cursor.setBufferPosition(startOfSelection)
+ }
+ }
+ }
+ super.select()
+ }
+
+ // * Purpose of pointInfoByCursor? see #235 for detail.
+ // When stayOnTransformString is enabled, cursor pos is not set on start of
+ // of selected range.
+ // But I want following behavior, so need to preserve position info.
+ // 1. `vj>.` -> indent same two rows regardless of current cursor's row.
+ // 2. `vj>j.` -> indent two rows from cursor's row.
+ for (const cursor of this.editor.getCursors()) {
+ const startOfSelection = cursor.selection.getBufferRange().start
+ this.onDidFinishOperation(() => {
+ cursorPosition = cursor.getBufferPosition()
+ this.pointInfoByCursor.set(cursor, {startOfSelection, cursorPosition})
+ })
+ }
+ }
+}
+CurrentSelection.register(false)
+
+class MoveLeft extends Motion {
+ moveCursor(cursor) {
+ const allowWrap = this.getConfig("wrapLeftRightMotion")
+ this.moveCursorCountTimes(cursor, () => moveCursorLeft(cursor, {allowWrap}))
+ }
+}
+MoveLeft.register()
+
+class MoveRight extends Motion {
+ canWrapToNextLine(cursor) {
+ if (this.isAsTargetExceptSelectInVisualMode() && !cursor.isAtEndOfLine()) {
+ return false
+ } else {
+ return this.getConfig("wrapLeftRightMotion")
+ }
+ }
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ const cursorPosition = cursor.getBufferPosition()
+ this.editor.unfoldBufferRow(cursorPosition.row)
+ const allowWrap = this.canWrapToNextLine(cursor)
+ moveCursorRight(cursor)
+ if (cursor.isAtEndOfLine() && allowWrap && !pointIsAtVimEndOfFile(this.editor, cursorPosition)) {
+ moveCursorRight(cursor, {allowWrap})
+ }
+ })
+ }
+}
+MoveRight.register()
+
+class MoveRightBufferColumn extends Motion {
+ moveCursor(cursor) {
+ setBufferColumn(cursor, cursor.getBufferColumn() + this.getCount())
+ }
+}
+MoveRightBufferColumn.register(false)
+
+class MoveUp extends Motion {
+ wise = "linewise"
+ wrap = false
+
+ getBufferRow(row) {
+ const min = 0
+ row = this.wrap && row === min ? this.getVimLastBufferRow() : limitNumber(row - 1, {min})
+ return this.getFoldStartRowForRow(row)
+ }
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => setBufferRow(cursor, this.getBufferRow(cursor.getBufferRow())))
+ }
+}
+MoveUp.register()
+
+class MoveUpWrap extends MoveUp {
+ wrap = true
+}
+MoveUpWrap.register()
+
+class MoveDown extends MoveUp {
+ wise = "linewise"
+ wrap = false
+
+ getBufferRow(row) {
+ if (this.editor.isFoldedAtBufferRow(row)) {
+ row = getLargestFoldRangeContainsBufferRow(this.editor, row).end.row
+ }
+ const max = this.getVimLastBufferRow()
+ return this.wrap && row >= max ? 0 : limitNumber(row + 1, {max})
+ }
+}
+MoveDown.register()
+
+class MoveDownWrap extends MoveDown {
+ wrap = true
+}
+MoveDownWrap.register()
+
+class MoveUpScreen extends Motion {
+ wise = "linewise"
+ direction = "up"
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => moveCursorUpScreen(cursor))
+ }
+}
+MoveUpScreen.register()
+
+class MoveDownScreen extends MoveUpScreen {
+ wise = "linewise"
+ direction = "down"
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => moveCursorDownScreen(cursor))
+ }
+}
+MoveDownScreen.register()
+
+// Move down/up to Edge
+// -------------------------
+// See t9md/atom-vim-mode-plus#236
+// At least v1.7.0. bufferPosition and screenPosition cannot convert accurately
+// when row is folded.
+class MoveUpToEdge extends Motion {
+ wise = "linewise"
+ jump = true
+ direction = "up"
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ this.setScreenPositionSafely(cursor, this.getPoint(cursor.getScreenPosition()))
+ })
+ }
+
+ getPoint(fromPoint) {
+ const {column} = fromPoint
+ for (const row of this.getScanRows(fromPoint)) {
+ const point = new Point(row, column)
+ if (this.isEdge(point)) return point
+ }
+ }
+
+ getScanRows({row}) {
+ return this.direction === "up"
+ ? getList(getValidVimScreenRow(this.editor, row - 1), 0, true)
+ : getList(getValidVimScreenRow(this.editor, row + 1), this.getVimLastScreenRow(), true)
+ }
+
+ isEdge(point) {
+ if (this.isStoppablePoint(point)) {
+ // If one of above/below point was not stoppable, it's Edge!
+ const above = point.translate([-1, 0])
+ const below = point.translate([+1, 0])
+ return !this.isStoppablePoint(above) || !this.isStoppablePoint(below)
+ } else {
+ return false
+ }
+ }
+
+ isStoppablePoint(point) {
+ if (this.isNonWhiteSpacePoint(point) || this.isFirstRowOrLastRowAndStoppable(point)) {
+ return true
+ } else {
+ const leftPoint = point.translate([0, -1])
+ const rightPoint = point.translate([0, +1])
+ return this.isNonWhiteSpacePoint(leftPoint) && this.isNonWhiteSpacePoint(rightPoint)
+ }
+ }
+
+ isNonWhiteSpacePoint(point) {
+ const char = getTextInScreenRange(this.editor, Range.fromPointWithDelta(point, 0, 1))
+ return char != null && /\S/.test(char)
+ }
+
+ isFirstRowOrLastRowAndStoppable(point) {
+ // In normal-mode we adjust cursor by moving-left if cursor at EOL of non-blank row.
+ // So explicitly guard to not answer it stoppable.
+ if (this.isMode("normal") && pointIsAtEndOfLineAtNonEmptyRow(this.editor, point)) {
+ return false
+ } else {
+ return (
+ point.isEqual(this.editor.clipScreenPosition(point)) &&
+ (point.row === 0 || point.row === this.getVimLastScreenRow())
+ )
+ }
+ }
+}
+MoveUpToEdge.register()
+
+class MoveDownToEdge extends MoveUpToEdge {
+ direction = "down"
+}
+MoveDownToEdge.register()
+
+// word
+// -------------------------
+class MoveToNextWord extends Motion {
+ wordRegex = null
+
+ getPoint(regex, from) {
+ let wordRange
+ let found = false
+
+ this.scanForward(regex, {from}, function({range, matchText, stop}) {
+ wordRange = range
+ // Ignore 'empty line' matches between '\r' and '\n'
+ if (matchText === "" && range.start.column !== 0) return
+ if (range.start.isGreaterThan(from)) {
+ found = true
+ stop()
+ }
+ })
+
+ if (found) {
+ const point = wordRange.start
+ return pointIsAtEndOfLineAtNonEmptyRow(this.editor, point) && !point.isEqual(this.getVimEofBufferPosition())
+ ? point.traverse([1, 0])
+ : point
+ } else {
+ return wordRange ? wordRange.end : from
+ }
+ }
+
+ // Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
+ // on a non-blank. This is because "cw" is interpreted as change-word, and a
+ // word does not include the following white space. {Vi: "cw" when on a blank
+ // followed by other blanks changes only the first blank; this is probably a
+ // bug, because "dw" deletes all the blanks}
+ //
+ // Another special case: When using the "w" motion in combination with an
+ // operator and the last word moved over is at the end of a line, the end of
+ // that word becomes the end of the operated text, not the first word in the
+ // next line.
+ moveCursor(cursor) {
+ const cursorPosition = cursor.getBufferPosition()
+ if (pointIsAtVimEndOfFile(this.editor, cursorPosition)) return
+
+ const wasOnWhiteSpace = pointIsOnWhiteSpace(this.editor, cursorPosition)
+ const isAsTargetExceptSelectInVisualMode = this.isAsTargetExceptSelectInVisualMode()
+
+ this.moveCursorCountTimes(cursor, ({isFinal}) => {
+ const cursorPosition = cursor.getBufferPosition()
+ if (isEmptyRow(this.editor, cursorPosition.row) && isAsTargetExceptSelectInVisualMode) {
+ cursor.setBufferPosition(cursorPosition.traverse([1, 0]))
+ } else {
+ const regex = this.wordRegex || cursor.wordRegExp()
+ let point = this.getPoint(regex, cursorPosition)
+ if (isFinal && isAsTargetExceptSelectInVisualMode) {
+ if (this.operator.is("Change") && !wasOnWhiteSpace) {
+ point = cursor.getEndOfCurrentWordBufferPosition({wordRegex: this.wordRegex})
+ } else {
+ point = Point.min(point, getEndOfLineForBufferRow(this.editor, cursorPosition.row))
+ }
+ }
+ cursor.setBufferPosition(point)
+ }
+ })
+ }
+}
+MoveToNextWord.register()
+
+// b
+class MoveToPreviousWord extends Motion {
+ wordRegex = null
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ const point = cursor.getBeginningOfCurrentWordBufferPosition({wordRegex: this.wordRegex})
+ cursor.setBufferPosition(point)
+ })
+ }
+}
+MoveToPreviousWord.register()
+
+class MoveToEndOfWord extends Motion {
+ wordRegex = null
+ inclusive = true
+
+ moveToNextEndOfWord(cursor) {
+ moveCursorToNextNonWhitespace(cursor)
+ const point = cursor.getEndOfCurrentWordBufferPosition({wordRegex: this.wordRegex}).translate([0, -1])
+ cursor.setBufferPosition(Point.min(point, this.getVimEofBufferPosition()))
+ }
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ const originalPoint = cursor.getBufferPosition()
+ this.moveToNextEndOfWord(cursor)
+ if (originalPoint.isEqual(cursor.getBufferPosition())) {
+ // Retry from right column if cursor was already on EndOfWord
+ cursor.moveRight()
+ this.moveToNextEndOfWord(cursor)
+ }
+ })
+ }
+}
+MoveToEndOfWord.register()
+
+// [TODO: Improve, accuracy]
+class MoveToPreviousEndOfWord extends MoveToPreviousWord {
+ inclusive = true
+
+ moveCursor(cursor) {
+ const wordRange = cursor.getCurrentWordBufferRange()
+ const cursorPosition = cursor.getBufferPosition()
+
+ // if we're in the middle of a word then we need to move to its start
+ let times = this.getCount()
+ if (cursorPosition.isGreaterThan(wordRange.start) && cursorPosition.isLessThan(wordRange.end)) {
+ times += 1
+ }
+
+ for (const i in getList(1, times)) {
+ const point = cursor.getBeginningOfCurrentWordBufferPosition({wordRegex: this.wordRegex})
+ cursor.setBufferPosition(point)
+ }
+
+ this.moveToNextEndOfWord(cursor)
+ if (cursor.getBufferPosition().isGreaterThanOrEqual(cursorPosition)) {
+ cursor.setBufferPosition([0, 0])
+ }
+ }
+
+ moveToNextEndOfWord(cursor) {
+ const point = cursor.getEndOfCurrentWordBufferPosition({wordRegex: this.wordRegex}).translate([0, -1])
+ cursor.setBufferPosition(Point.min(point, this.getVimEofBufferPosition()))
+ }
+}
+MoveToPreviousEndOfWord.register()
+
+// Whole word
+// -------------------------
+class MoveToNextWholeWord extends MoveToNextWord {
+ wordRegex = /^$|\S+/g
+}
+MoveToNextWholeWord.register()
+
+class MoveToPreviousWholeWord extends MoveToPreviousWord {
+ wordRegex = /^$|\S+/g
+}
+MoveToPreviousWholeWord.register()
+
+class MoveToEndOfWholeWord extends MoveToEndOfWord {
+ wordRegex = /\S+/
+}
+MoveToEndOfWholeWord.register()
+
+// [TODO: Improve, accuracy]
+class MoveToPreviousEndOfWholeWord extends MoveToPreviousEndOfWord {
+ wordRegex = /\S+/
+}
+MoveToPreviousEndOfWholeWord.register()
+
+// Alphanumeric word [Experimental]
+// -------------------------
+class MoveToNextAlphanumericWord extends MoveToNextWord {
+ wordRegex = /\w+/g
+}
+MoveToNextAlphanumericWord.register()
+
+class MoveToPreviousAlphanumericWord extends MoveToPreviousWord {
+ wordRegex = /\w+/
+}
+MoveToPreviousAlphanumericWord.register()
+
+class MoveToEndOfAlphanumericWord extends MoveToEndOfWord {
+ wordRegex = /\w+/
+}
+MoveToEndOfAlphanumericWord.register()
+
+// Alphanumeric word [Experimental]
+// -------------------------
+class MoveToNextSmartWord extends MoveToNextWord {
+ wordRegex = /[\w-]+/g
+}
+MoveToNextSmartWord.register()
+
+class MoveToPreviousSmartWord extends MoveToPreviousWord {
+ wordRegex = /[\w-]+/
+}
+MoveToPreviousSmartWord.register()
+
+class MoveToEndOfSmartWord extends MoveToEndOfWord {
+ wordRegex = /[\w-]+/
+}
+MoveToEndOfSmartWord.register()
+
+// Subword
+// -------------------------
+class MoveToNextSubword extends MoveToNextWord {
+ moveCursor(cursor) {
+ this.wordRegex = cursor.subwordRegExp()
+ super.moveCursor(cursor)
+ }
+}
+MoveToNextSubword.register()
+
+class MoveToPreviousSubword extends MoveToPreviousWord {
+ moveCursor(cursor) {
+ this.wordRegex = cursor.subwordRegExp()
+ super.moveCursor(cursor)
+ }
+}
+MoveToPreviousSubword.register()
+
+class MoveToEndOfSubword extends MoveToEndOfWord {
+ moveCursor(cursor) {
+ this.wordRegex = cursor.subwordRegExp()
+ super.moveCursor(cursor)
+ }
+}
+MoveToEndOfSubword.register()
+
+// Sentence
+// -------------------------
+// Sentence is defined as below
+// - end with ['.', '!', '?']
+// - optionally followed by [')', ']', '"', "'"]
+// - followed by ['$', ' ', '\t']
+// - paragraph boundary is also sentence boundary
+// - section boundary is also sentence boundary(ignore)
+class MoveToNextSentence extends Motion {
+ jump = true
+ sentenceRegex = new RegExp(`(?:[\\.!\\?][\\)\\]"']*\\s+)|(\\n|\\r\\n)`, "g")
+ direction = "next"
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ this.setBufferPositionSafely(cursor, this.getPoint(cursor.getBufferPosition()))
+ })
+ }
+
+ getPoint(fromPoint) {
+ if (this.direction === "next") {
+ return this.getNextStartOfSentence(fromPoint)
+ } else if (this.direction === "previous") {
+ return this.getPreviousStartOfSentence(fromPoint)
+ }
+ }
+
+ isBlankRow(row) {
+ return this.editor.isBufferRowBlank(row)
+ }
+
+ getNextStartOfSentence(from) {
+ let foundPoint
+ this.scanForward(this.sentenceRegex, {from}, ({range, matchText, match, stop}) => {
+ if (match[1] != null) {
+ const [startRow, endRow] = Array.from([range.start.row, range.end.row])
+ if (this.skipBlankRow && this.isBlankRow(endRow)) return
+ if (this.isBlankRow(startRow) !== this.isBlankRow(endRow)) {
+ foundPoint = this.getFirstCharacterPositionForBufferRow(endRow)
+ }
+ } else {
+ foundPoint = range.end
+ }
+ if (foundPoint) stop()
+ })
+ return foundPoint || this.getVimEofBufferPosition()
+ }
+
+ getPreviousStartOfSentence(from) {
+ let foundPoint
+ this.scanBackward(this.sentenceRegex, {from}, ({range, match, stop, matchText}) => {
+ if (match[1] != null) {
+ const [startRow, endRow] = Array.from([range.start.row, range.end.row])
+ if (!this.isBlankRow(endRow) && this.isBlankRow(startRow)) {
+ const point = this.getFirstCharacterPositionForBufferRow(endRow)
+ if (point.isLessThan(from)) {
+ foundPoint = point
+ } else {
+ if (this.skipBlankRow) return
+ foundPoint = this.getFirstCharacterPositionForBufferRow(startRow)
+ }
+ }
+ } else {
+ if (range.end.isLessThan(from)) foundPoint = range.end
+ }
+ if (foundPoint) stop()
+ })
+ return foundPoint || [0, 0]
+ }
+}
+MoveToNextSentence.register()
+
+class MoveToPreviousSentence extends MoveToNextSentence {
+ direction = "previous"
+}
+MoveToPreviousSentence.register()
+
+class MoveToNextSentenceSkipBlankRow extends MoveToNextSentence {
+ skipBlankRow = true
+}
+MoveToNextSentenceSkipBlankRow.register()
+
+class MoveToPreviousSentenceSkipBlankRow extends MoveToPreviousSentence {
+ skipBlankRow = true
+}
+MoveToPreviousSentenceSkipBlankRow.register()
+
+// Paragraph
+// -------------------------
+class MoveToNextParagraph extends Motion {
+ jump = true
+ direction = "next"
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ this.setBufferPositionSafely(cursor, this.getPoint(cursor.getBufferPosition()))
+ })
+ }
+
+ getPoint(fromPoint) {
+ const startRow = fromPoint.row
+ let wasAtNonBlankRow = !this.editor.isBufferRowBlank(startRow)
+ for (let row of getBufferRows(this.editor, {startRow, direction: this.direction})) {
+ if (this.editor.isBufferRowBlank(row)) {
+ if (wasAtNonBlankRow) return new Point(row, 0)
+ } else {
+ wasAtNonBlankRow = true
+ }
+ }
+
+ // fallback
+ return this.direction === "previous" ? new Point(0, 0) : this.getVimEofBufferPosition()
+ }
+}
+MoveToNextParagraph.register()
+
+class MoveToPreviousParagraph extends MoveToNextParagraph {
+ direction = "previous"
+}
+MoveToPreviousParagraph.register()
+
+// -------------------------
+// keymap: 0
+class MoveToBeginningOfLine extends Motion {
+ moveCursor(cursor) {
+ setBufferColumn(cursor, 0)
+ }
+}
+MoveToBeginningOfLine.register()
+
+class MoveToColumn extends Motion {
+ moveCursor(cursor) {
+ setBufferColumn(cursor, this.getCount(-1))
+ }
+}
+MoveToColumn.register()
+
+class MoveToLastCharacterOfLine extends Motion {
+ moveCursor(cursor) {
+ const row = getValidVimBufferRow(this.editor, cursor.getBufferRow() + this.getCount(-1))
+ cursor.setBufferPosition([row, Infinity])
+ cursor.goalColumn = Infinity
+ }
+}
+MoveToLastCharacterOfLine.register()
+
+class MoveToLastNonblankCharacterOfLineAndDown extends Motion {
+ inclusive = true
+
+ moveCursor(cursor) {
+ cursor.setBufferPosition(this.getPoint(cursor.getBufferPosition()))
+ }
+
+ getPoint({row}) {
+ row = limitNumber(row + this.getCount(-1), {max: this.getVimLastBufferRow()})
+ const range = findRangeInBufferRow(this.editor, /\S|^/, row, {direction: "backward"})
+ return range ? range.start : new Point(row, 0)
+ }
+}
+MoveToLastNonblankCharacterOfLineAndDown.register()
+
+// MoveToFirstCharacterOfLine faimily
+// ------------------------------------
+// ^
+class MoveToFirstCharacterOfLine extends Motion {
+ moveCursor(cursor) {
+ const point = this.getFirstCharacterPositionForBufferRow(cursor.getBufferRow())
+ this.setBufferPositionSafely(cursor, point)
+ }
+}
+MoveToFirstCharacterOfLine.register()
+
+class MoveToFirstCharacterOfLineUp extends MoveToFirstCharacterOfLine {
+ wise = "linewise"
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, function() {
+ const point = cursor.getBufferPosition()
+ if (point.row > 0) {
+ cursor.setBufferPosition(point.translate([-1, 0]))
+ }
+ })
+ super.moveCursor(cursor)
+ }
+}
+MoveToFirstCharacterOfLineUp.register()
+
+class MoveToFirstCharacterOfLineDown extends MoveToFirstCharacterOfLine {
+ wise = "linewise"
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ const point = cursor.getBufferPosition()
+ if (point.row < this.getVimLastBufferRow()) {
+ cursor.setBufferPosition(point.translate([+1, 0]))
+ }
+ })
+ super.moveCursor(cursor)
+ }
+}
+MoveToFirstCharacterOfLineDown.register()
+
+class MoveToFirstCharacterOfLineAndDown extends MoveToFirstCharacterOfLineDown {
+ getCount() {
+ return super.getCount(-1)
+ }
+}
+MoveToFirstCharacterOfLineAndDown.register()
+
+// keymap: g g
+class MoveToFirstLine extends Motion {
+ wise = "linewise"
+ jump = true
+ verticalMotion = true
+ moveSuccessOnLinewise = true
+
+ moveCursor(cursor) {
+ this.setCursorBufferRow(cursor, getValidVimBufferRow(this.editor, this.getRow()))
+ cursor.autoscroll({center: true})
+ }
+
+ getRow() {
+ return this.getCount(-1)
+ }
+}
+MoveToFirstLine.register()
+
+class MoveToScreenColumn extends Motion {
+ moveCursor(cursor) {
+ const allowOffScreenPosition = this.getConfig("allowMoveToOffScreenColumnOnScreenLineMotion")
+ const point = getScreenPositionForScreenRow(this.editor, cursor.getScreenRow(), this.which, {
+ allowOffScreenPosition,
+ })
+ this.setScreenPositionSafely(cursor, point)
+ }
+}
+MoveToScreenColumn.register(false)
+
+// keymap: g 0
+class MoveToBeginningOfScreenLine extends MoveToScreenColumn {
+ which = "beginning"
+}
+MoveToBeginningOfScreenLine.register()
+
+// g ^: `move-to-first-character-of-screen-line`
+class MoveToFirstCharacterOfScreenLine extends MoveToScreenColumn {
+ which = "first-character"
+}
+MoveToFirstCharacterOfScreenLine.register()
+
+// keymap: g $
+class MoveToLastCharacterOfScreenLine extends MoveToScreenColumn {
+ which = "last-character"
+}
+MoveToLastCharacterOfScreenLine.register()
+
+// keymap: G
+class MoveToLastLine extends MoveToFirstLine {
+ defaultCount = Infinity
+}
+MoveToLastLine.register()
+
+// keymap: N% e.g. 10%
+class MoveToLineByPercent extends MoveToFirstLine {
+ getRow() {
+ const percent = limitNumber(this.getCount(), {max: 100})
+ return Math.floor((this.editor.getLineCount() - 1) * (percent / 100))
+ }
+}
+MoveToLineByPercent.register()
+
+class MoveToRelativeLine extends Motion {
+ wise = "linewise"
+ moveSuccessOnLinewise = true
+
+ moveCursor(cursor) {
+ let row
+ let count = this.getCount()
+ if (count < 0) {
+ // Support negative count
+ // Negative count can be passed like `operationStack.run("MoveToRelativeLine", {count: -5})`.
+ // Currently used in vim-mode-plus-ex-mode pkg.
+ count += 1
+ row = this.getFoldStartRowForRow(cursor.getBufferRow())
+ while (count++ < 0) row = this.getFoldStartRowForRow(row - 1)
+ } else {
+ count -= 1
+ row = this.getFoldEndRowForRow(cursor.getBufferRow())
+ while (count-- > 0) row = this.getFoldEndRowForRow(row + 1)
+ }
+ setBufferRow(cursor, row)
+ }
+}
+MoveToRelativeLine.register(false)
+
+class MoveToRelativeLineMinimumTwo extends MoveToRelativeLine {
+ getCount(...args) {
+ return limitNumber(super.getCount(...args), {min: 2})
+ }
+}
+MoveToRelativeLineMinimumTwo.register(false)
+
+// Position cursor without scrolling., H, M, L
+// -------------------------
+// keymap: H
+class MoveToTopOfScreen extends Motion {
+ wise = "linewise"
+ jump = true
+ scrolloff = 2
+ defaultCount = 0
+ verticalMotion = true
+
+ moveCursor(cursor) {
+ const bufferRow = this.editor.bufferRowForScreenRow(this.getScreenRow())
+ this.setCursorBufferRow(cursor, bufferRow)
+ }
+
+ getScrolloff() {
+ return this.isAsTargetExceptSelectInVisualMode() ? 0 : this.scrolloff
+ }
+
+ getScreenRow() {
+ const firstRow = getFirstVisibleScreenRow(this.editor)
+ let offset = this.getScrolloff()
+ if (firstRow === 0) {
+ offset = 0
+ }
+ offset = limitNumber(this.getCount(-1), {min: offset})
+ return firstRow + offset
+ }
+}
+MoveToTopOfScreen.register()
+
+// keymap: M
+class MoveToMiddleOfScreen extends MoveToTopOfScreen {
+ getScreenRow() {
+ const startRow = getFirstVisibleScreenRow(this.editor)
+ const endRow = limitNumber(this.editor.getLastVisibleScreenRow(), {max: this.getVimLastScreenRow()})
+ return startRow + Math.floor((endRow - startRow) / 2)
+ }
+}
+MoveToMiddleOfScreen.register()
+
+// keymap: L
+class MoveToBottomOfScreen extends MoveToTopOfScreen {
+ getScreenRow() {
+ // [FIXME]
+ // At least Atom v1.6.0, there are two implementation of getLastVisibleScreenRow()
+ // editor.getLastVisibleScreenRow() and editorElement.getLastVisibleScreenRow()
+ // Those two methods return different value, editor's one is corrent.
+ // So I intentionally use editor.getLastScreenRow here.
+ const vimLastScreenRow = this.getVimLastScreenRow()
+ const row = limitNumber(this.editor.getLastVisibleScreenRow(), {max: vimLastScreenRow})
+ let offset = this.getScrolloff() + 1
+ if (row === vimLastScreenRow) {
+ offset = 0
+ }
+ offset = limitNumber(this.getCount(-1), {min: offset})
+ return row - offset
+ }
+}
+MoveToBottomOfScreen.register()
+
+// Scrolling
+// Half: ctrl-d, ctrl-u
+// Full: ctrl-f, ctrl-b
+// -------------------------
+// [FIXME] count behave differently from original Vim.
+class Scroll extends Motion {
+ verticalMotion = true
+
+ isSmoothScrollEnabled() {
+ return Math.abs(this.amountOfPage) === 1
+ ? this.getConfig("smoothScrollOnFullScrollMotion")
+ : this.getConfig("smoothScrollOnHalfScrollMotion")
+ }
+
+ getSmoothScrollDuation() {
+ return Math.abs(this.amountOfPage) === 1
+ ? this.getConfig("smoothScrollOnFullScrollMotionDuration")
+ : this.getConfig("smoothScrollOnHalfScrollMotionDuration")
+ }
+
+ getPixelRectTopForSceenRow(row) {
+ const point = new Point(row, 0)
+ return this.editor.element.pixelRectForScreenRange(new Range(point, point)).top
+ }
+
+ smoothScroll(fromRow, toRow, done) {
+ const topPixelFrom = {top: this.getPixelRectTopForSceenRow(fromRow)}
+ const topPixelTo = {top: this.getPixelRectTopForSceenRow(toRow)}
+ // [NOTE]
+ // intentionally use `element.component.setScrollTop` instead of `element.setScrollTop`.
+ // SInce element.setScrollTop will throw exception when element.component no longer exists.
+ const step = newTop => {
+ if (this.editor.element.component) {
+ this.editor.element.component.setScrollTop(newTop)
+ this.editor.element.component.updateSync()
+ }
+ }
+
+ const duration = this.getSmoothScrollDuation()
+ this.vimState.requestScrollAnimation(topPixelFrom, topPixelTo, {duration, step, done})
+ }
+
+ getAmountOfRows() {
+ return Math.ceil(this.amountOfPage * this.editor.getRowsPerPage() * this.getCount())
+ }
+
+ getBufferRow(cursor) {
+ const screenRow = getValidVimScreenRow(this.editor, cursor.getScreenRow() + this.getAmountOfRows())
+ return this.editor.bufferRowForScreenRow(screenRow)
+ }
+
+ moveCursor(cursor) {
+ const bufferRow = this.getBufferRow(cursor)
+ this.setCursorBufferRow(cursor, this.getBufferRow(cursor), {autoscroll: false})
+
+ if (cursor.isLastCursor()) {
+ if (this.isSmoothScrollEnabled()) this.vimState.finishScrollAnimation()
+
+ const firstVisibileScreenRow = this.editor.getFirstVisibleScreenRow()
+ const newFirstVisibileBufferRow = this.editor.bufferRowForScreenRow(
+ firstVisibileScreenRow + this.getAmountOfRows()
+ )
+ const newFirstVisibileScreenRow = this.editor.screenRowForBufferRow(newFirstVisibileBufferRow)
+ const done = () => {
+ this.editor.setFirstVisibleScreenRow(newFirstVisibileScreenRow)
+ // [FIXME] sometimes, scrollTop is not updated, calling this fix.
+ // Investigate and find better approach then remove this workaround.
+ if (this.editor.element.component) this.editor.element.component.updateSync()
+ }
+
+ if (this.isSmoothScrollEnabled()) this.smoothScroll(firstVisibileScreenRow, newFirstVisibileScreenRow, done)
+ else done()
+ }
+ }
+}
+Scroll.register(false)
+
+// keymap: ctrl-f
+class ScrollFullScreenDown extends Scroll {
+ amountOfPage = +1
+}
+ScrollFullScreenDown.register()
+
+// keymap: ctrl-b
+class ScrollFullScreenUp extends Scroll {
+ amountOfPage = -1
+}
+ScrollFullScreenUp.register()
+
+// keymap: ctrl-d
+class ScrollHalfScreenDown extends Scroll {
+ amountOfPage = +1 / 2
+}
+ScrollHalfScreenDown.register()
+
+// keymap: ctrl-u
+class ScrollHalfScreenUp extends Scroll {
+ amountOfPage = -1 / 2
+}
+ScrollHalfScreenUp.register()
+
+// Find
+// -------------------------
+// keymap: f
+class Find extends Motion {
+ backwards = false
+ inclusive = true
+ offset = 0
+ requireInput = true
+ caseSensitivityKind = "Find"
+
+ restoreEditorState() {
+ if (this._restoreEditorState) this._restoreEditorState()
+ this._restoreEditorState = null
+ }
+
+ cancelOperation() {
+ this.restoreEditorState()
+ super.cancelOperation()
+ }
+
+ initialize() {
+ if (this.getConfig("reuseFindForRepeatFind")) this.repeatIfNecessary()
+ if (!this.isComplete()) {
+ const charsMax = this.getConfig("findCharsMax")
+ const optionsBase = {purpose: "find", charsMax}
+
+ if (charsMax === 1) {
+ this.focusInput(optionsBase)
+ } else {
+ this._restoreEditorState = saveEditorState(this.editor)
+ const options = {
+ autoConfirmTimeout: this.getConfig("findConfirmByTimeout"),
+ onConfirm: input => {
+ this.input = input
+ if (input) this.processOperation()
+ else this.cancelOperation()
+ },
+ onChange: preConfirmedChars => {
+ this.preConfirmedChars = preConfirmedChars
+ this.highlightTextInCursorRows(this.preConfirmedChars, "pre-confirm")
+ },
+ onCancel: () => {
+ this.vimState.highlightFind.clearMarkers()
+ this.cancelOperation()
+ },
+ commands: {
+ "vim-mode-plus:find-next-pre-confirmed": () => this.findPreConfirmed(+1),
+ "vim-mode-plus:find-previous-pre-confirmed": () => this.findPreConfirmed(-1),
+ },
+ }
+ this.focusInput(Object.assign(options, optionsBase))
+ }
+ }
+ return super.initialize()
+ }
+
+ findPreConfirmed(delta) {
+ if (this.preConfirmedChars && this.getConfig("highlightFindChar")) {
+ const index = this.highlightTextInCursorRows(
+ this.preConfirmedChars,
+ "pre-confirm",
+ this.getCount(-1) + delta,
+ true
+ )
+ this.count = index + 1
+ }
+ }
+
+ repeatIfNecessary() {
+ const findCommandNames = ["Find", "FindBackwards", "Till", "TillBackwards"]
+ const currentFind = this.globalState.get("currentFind")
+ if (currentFind && findCommandNames.includes(this.vimState.operationStack.getLastCommandName())) {
+ this.input = currentFind.input
+ this.repeated = true
+ }
+ }
+
+ isBackwards() {
+ return this.backwards
+ }
+
+ execute() {
+ super.execute()
+ let decorationType = "post-confirm"
+ if (this.operator != null && !(this.operator != null ? this.operator.instanceof("SelectBase") : undefined)) {
+ decorationType += " long"
+ }
+ this.editor.component.getNextUpdatePromise().then(() => {
+ this.highlightTextInCursorRows(this.input, decorationType)
+ })
+ }
+
+ getPoint(fromPoint) {
+ const scanRange = this.editor.bufferRangeForBufferRow(fromPoint.row)
+ const points = []
+ const regex = this.getRegex(this.input)
+ const indexWantAccess = this.getCount(-1)
+
+ const translation = new Point(0, this.isBackwards() ? this.offset : -this.offset)
+ if (this.repeated) {
+ fromPoint = fromPoint.translate(translation.negate())
+ }
+
+ if (this.isBackwards()) {
+ if (this.getConfig("findAcrossLines")) scanRange.start = Point.ZERO
+
+ this.editor.backwardsScanInBufferRange(regex, scanRange, ({range, stop}) => {
+ if (range.start.isLessThan(fromPoint)) {
+ points.push(range.start)
+ if (points.length > indexWantAccess) {
+ stop()
+ }
+ }
+ })
+ } else {
+ if (this.getConfig("findAcrossLines")) scanRange.end = this.editor.getEofBufferPosition()
+ this.editor.scanInBufferRange(regex, scanRange, ({range, stop}) => {
+ if (range.start.isGreaterThan(fromPoint)) {
+ points.push(range.start)
+ if (points.length > indexWantAccess) {
+ stop()
+ }
+ }
+ })
+ }
+
+ const point = points[indexWantAccess]
+ if (point) return point.translate(translation)
+ }
+
+ // FIXME: bad naming, this function must return index
+ highlightTextInCursorRows(text, decorationType, index = this.getCount(-1), adjustIndex = false) {
+ if (!this.getConfig("highlightFindChar")) return
+
+ return this.vimState.highlightFind.highlightCursorRows(
+ this.getRegex(text),
+ decorationType,
+ this.isBackwards(),
+ this.offset,
+ index,
+ adjustIndex
+ )
+ }
+
+ moveCursor(cursor) {
+ const point = this.getPoint(cursor.getBufferPosition())
+ if (point) cursor.setBufferPosition(point)
+ else this.restoreEditorState()
+
+ if (!this.repeated) this.globalState.set("currentFind", this)
+ }
+
+ getRegex(term) {
+ const modifiers = this.isCaseSensitive(term) ? "g" : "gi"
+ return new RegExp(_.escapeRegExp(term), modifiers)
+ }
+}
+Find.register()
+
+// keymap: F
+class FindBackwards extends Find {
+ inclusive = false
+ backwards = true
+}
+FindBackwards.register()
+
+// keymap: t
+class Till extends Find {
+ offset = 1
+ getPoint(...args) {
+ const point = super.getPoint(...args)
+ this.moveSucceeded = point != null
+ return point
+ }
+}
+Till.register()
+
+// keymap: T
+class TillBackwards extends Till {
+ inclusive = false
+ backwards = true
+}
+TillBackwards.register()
+
+// Mark
+// -------------------------
+// keymap: `
+class MoveToMark extends Motion {
+ jump = true
+ requireInput = true
+ input = null
+
+ initialize() {
+ if (!this.isComplete()) this.readChar()
+ return super.initialize()
+ }
+
+ getPoint() {
+ return this.vimState.mark.get(this.input)
+ }
+
+ moveCursor(cursor) {
+ const point = this.getPoint()
+ if (point) {
+ cursor.setBufferPosition(point)
+ cursor.autoscroll({center: true})
+ }
+ }
+}
+MoveToMark.register()
+
+// keymap: '
+class MoveToMarkLine extends MoveToMark {
+ wise = "linewise"
+
+ getPoint() {
+ const point = super.getPoint()
+ if (point) {
+ return this.getFirstCharacterPositionForBufferRow(point.row)
+ }
+ }
+}
+MoveToMarkLine.register()
+
+// Fold
+// -------------------------
+class MoveToPreviousFoldStart extends Motion {
+ wise = "characterwise"
+ which = "start"
+ direction = "prev"
+
+ execute() {
+ this.rows = this.getFoldRows(this.which)
+ if (this.direction === "prev") this.rows.reverse()
+
+ super.execute()
+ }
+
+ getFoldRows(which) {
+ const index = which === "start" ? 0 : 1
+ const rows = getCodeFoldRowRanges(this.editor).map(rowRange => rowRange[index])
+ return _.sortBy(_.uniq(rows), row => row)
+ }
+
+ getScanRows(cursor) {
+ const cursorRow = cursor.getBufferRow()
+ const isVald = this.direction === "prev" ? row => row < cursorRow : row => row > cursorRow
+ return this.rows.filter(isVald)
+ }
+
+ detectRow(cursor) {
+ return this.getScanRows(cursor)[0]
+ }
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ const row = this.detectRow(cursor)
+ if (row != null) moveCursorToFirstCharacterAtRow(cursor, row)
+ })
+ }
+}
+MoveToPreviousFoldStart.register()
+
+class MoveToNextFoldStart extends MoveToPreviousFoldStart {
+ direction = "next"
+}
+MoveToNextFoldStart.register()
+
+class MoveToPreviousFoldStartWithSameIndent extends MoveToPreviousFoldStart {
+ detectRow(cursor) {
+ const baseIndentLevel = this.editor.indentationForBufferRow(cursor.getBufferRow())
+ return this.getScanRows(cursor).find(row => this.editor.indentationForBufferRow(row) === baseIndentLevel)
+ }
+}
+MoveToPreviousFoldStartWithSameIndent.register()
+
+class MoveToNextFoldStartWithSameIndent extends MoveToPreviousFoldStartWithSameIndent {
+ direction = "next"
+}
+MoveToNextFoldStartWithSameIndent.register()
+
+class MoveToPreviousFoldEnd extends MoveToPreviousFoldStart {
+ which = "end"
+}
+MoveToPreviousFoldEnd.register()
+
+class MoveToNextFoldEnd extends MoveToPreviousFoldEnd {
+ direction = "next"
+}
+MoveToNextFoldEnd.register()
+
+// -------------------------
+class MoveToPreviousFunction extends MoveToPreviousFoldStart {
+ direction = "prev"
+ detectRow(cursor) {
+ return this.getScanRows(cursor).find(row => isIncludeFunctionScopeForRow(this.editor, row))
+ }
+}
+MoveToPreviousFunction.register()
+
+class MoveToNextFunction extends MoveToPreviousFunction {
+ direction = "next"
+}
+MoveToNextFunction.register()
+
+// Scope based
+// -------------------------
+class MoveToPositionByScope extends Motion {
+ direction = "backward"
+ scope = "."
+
+ getPoint(fromPoint) {
+ return detectScopeStartPositionForScope(this.editor, fromPoint, this.direction, this.scope)
+ }
+
+ moveCursor(cursor) {
+ this.moveCursorCountTimes(cursor, () => {
+ this.setBufferPositionSafely(cursor, this.getPoint(cursor.getBufferPosition()))
+ })
+ }
+}
+MoveToPositionByScope.register(false)
+
+class MoveToPreviousString extends MoveToPositionByScope {
+ direction = "backward"
+ scope = "string.begin"
+}
+MoveToPreviousString.register()
+
+class MoveToNextString extends MoveToPreviousString {
+ direction = "forward"
+}
+MoveToNextString.register()
+
+class MoveToPreviousNumber extends MoveToPositionByScope {
+ direction = "backward"
+ scope = "constant.numeric"
+}
+MoveToPreviousNumber.register()
+
+class MoveToNextNumber extends MoveToPreviousNumber {
+ direction = "forward"
+}
+MoveToNextNumber.register()
+
+class MoveToNextOccurrence extends Motion {
+ // Ensure this command is available when only has-occurrence
+ static commandScope = "atom-text-editor.vim-mode-plus.has-occurrence"
+ jump = true
+ direction = "next"
+
+ execute() {
+ this.ranges = sortRanges(this.occurrenceManager.getMarkers().map(marker => marker.getBufferRange()))
+ super.execute()
+ }
+
+ moveCursor(cursor) {
+ const range = this.ranges[getIndex(this.getIndex(cursor.getBufferPosition()), this.ranges)]
+ const point = range.start
+ cursor.setBufferPosition(point, {autoscroll: false})
+
+ if (cursor.isLastCursor()) {
+ this.editor.unfoldBufferRow(point.row)
+ smartScrollToBufferPosition(this.editor, point)
+ }
+
+ if (this.getConfig("flashOnMoveToOccurrence")) {
+ this.vimState.flash(range, {type: "search"})
+ }
+ }
+
+ getIndex(fromPoint) {
+ const index = this.ranges.findIndex(range => range.start.isGreaterThan(fromPoint))
+ return (index >= 0 ? index : 0) + this.getCount(-1)
+ }
+}
+MoveToNextOccurrence.register()
+
+class MoveToPreviousOccurrence extends MoveToNextOccurrence {
+ direction = "previous"
+
+ getIndex(fromPoint) {
+ const ranges = this.ranges.slice().reverse()
+ const range = ranges.find(range => range.end.isLessThan(fromPoint))
+ const index = range ? this.ranges.indexOf(range) : this.ranges.length - 1
+ return index - this.getCount(-1)
+ }
+}
+MoveToPreviousOccurrence.register()
+
+// -------------------------
+// keymap: %
+class MoveToPair extends Motion {
+ inclusive = true
+ jump = true
+ member = ["Parenthesis", "CurlyBracket", "SquareBracket"]
+
+ moveCursor(cursor) {
+ this.setBufferPositionSafely(cursor, this.getPoint(cursor))
+ }
+
+ getPointForTag(point) {
+ const pairInfo = this.getInstance("ATag").getPairInfo(point)
+ if (!pairInfo) return
+
+ let {openRange, closeRange} = pairInfo
+ openRange = openRange.translate([0, +1], [0, -1])
+ closeRange = closeRange.translate([0, +1], [0, -1])
+ if (openRange.containsPoint(point) && !point.isEqual(openRange.end)) {
+ return closeRange.start
+ }
+ if (closeRange.containsPoint(point) && !point.isEqual(closeRange.end)) {
+ return openRange.start
+ }
+ }
+
+ getPoint(cursor) {
+ const cursorPosition = cursor.getBufferPosition()
+ const cursorRow = cursorPosition.row
+ const point = this.getPointForTag(cursorPosition)
+ if (point) return point
+
+ // AAnyPairAllowForwarding return forwarding range or enclosing range.
+ const range = this.getInstance("AAnyPairAllowForwarding", {member: this.member}).getRange(cursor.selection)
+ if (!range) return
+
+ const {start, end} = range
+ if (start.row === cursorRow && start.isGreaterThanOrEqual(cursorPosition)) {
+ // Forwarding range found
+ return end.translate([0, -1])
+ } else if (end.row === cursorPosition.row) {
+ // Enclosing range was returned
+ // We move to start( open-pair ) only when close-pair was at same row as cursor-row.
+ return start
+ }
+ }
+}
+MoveToPair.register()
diff --git a/lib/occurrence-manager.js b/lib/occurrence-manager.js
index 3a3c1f5db..77d9146bf 100644
--- a/lib/occurrence-manager.js
+++ b/lib/occurrence-manager.js
@@ -57,7 +57,7 @@ module.exports = class OccurrenceManager {
return true
} else {
const options = {
- message: `Too many(${length}) matches. Do you want to continue?`,
+ message: `Too many(${length}) occurrences. Do you want to continue?`,
detailedMessage: `If you want increase threshold(current: ${threshold}), change "Confirm Threshold On Create Preset Occurrences" configuration.`,
buttons: ["Continue", "Cancel"],
}
diff --git a/lib/operation-stack.js b/lib/operation-stack.js
index af2054756..e98080721 100644
--- a/lib/operation-stack.js
+++ b/lib/operation-stack.js
@@ -1,6 +1,6 @@
const {Disposable, CompositeDisposable} = require("atom")
const Base = require("./base")
-let OperationAbortedError, SelectInVisualMode, MoveToRelativeLine
+let OperationAbortedError
// opration life in operationStack
// 1. run
@@ -67,16 +67,6 @@ module.exports = class OperationStack {
return this.stack.length === 0
}
- newMoveToRelativeLine() {
- if (!MoveToRelativeLine) MoveToRelativeLine = Base.getClass("MoveToRelativeLine")
- return new MoveToRelativeLine(this.vimState)
- }
-
- newSelectWithTarget(target) {
- if (!SelectInVisualMode) SelectInVisualMode = Base.getClass("SelectInVisualMode")
- return new SelectInVisualMode(this.vimState).setTarget(target)
- }
-
// Main
// -------------------------
run(klass, properties) {
@@ -101,15 +91,14 @@ module.exports = class OperationStack {
const stackTop = this.peekTop()
if (stackTop && stackTop.constructor === klass) {
// Replace operator when identical one repeated, e.g. `dd`, `cc`, `gUgU`
- operation = this.newMoveToRelativeLine()
- } else {
- operation = new klass(this.vimState, properties)
+ klass = "MoveToRelativeLine"
}
+ operation = Base.getInstance(this.vimState, klass, properties)
}
if (this.isEmpty()) {
if ((this.mode === "visual" && operation.isMotion()) || operation.isTextObject()) {
- operation = this.newSelectWithTarget(operation)
+ operation = Base.getInstance(this.vimState, "SelectInVisualMode").setTarget(operation)
}
this.stack.push(operation)
this.process()
@@ -142,15 +131,20 @@ module.exports = class OperationStack {
this.run(operation)
}
- runRecordedMotion(key, {reverse} = {}) {
- const recoded = this.vimState.globalState.get(key)
- if (!recoded) return
+ // Currently used in repeat-search and repeat-find("n", "N", ";", ",").
+ runRecordedMotion(key, {reverse = false} = {}) {
+ const recorded = this.vimState.globalState.get(key)
+ if (!recorded) return
- const operation = recoded.clone(this.vimState)
- operation.repeated = true
- operation.resetCount()
- if (reverse) operation.backwards = !operation.backwards
- this.run(operation)
+ recorded.vimState = this.vimState
+ recorded.repeated = true
+ recorded.resetCount()
+
+ const originalBackwards = recorded.backwards
+ if (reverse) recorded.backwards = !originalBackwards
+
+ this.run(recorded)
+ recorded.backwards = originalBackwards
}
runCurrentFind(options) {
@@ -189,23 +183,13 @@ module.exports = class OperationStack {
if (this.mode === "normal" && top.isOperator()) {
this.modeManager.activate("operator-pending")
}
- this.addOperatorSpecificPendingScope(top)
+ // Temporary set while command is running to achieve operation-specific keymap scopes
+ this.addToClassList(top.getCommandNameWithoutPrefix() + "-pending")
} else {
this.execute(this.stack.pop())
}
}
- addOperatorSpecificPendingScope(operation) {
- // Temporary set while command is running
- const commandName =
- typeof operation.constructor.getCommandNameWithoutPrefix === "function"
- ? operation.constructor.getCommandNameWithoutPrefix()
- : undefined
- if (commandName) {
- this.addToClassList(commandName + "-pending")
- }
- }
-
execute(operation) {
// Intentionally avoild wrapping by Promise.resolve() to make test easy.
// Since almost all command don't return promise, finish synchronously.
diff --git a/lib/operator-insert.coffee b/lib/operator-insert.coffee
deleted file mode 100644
index 2e4721e70..000000000
--- a/lib/operator-insert.coffee
+++ /dev/null
@@ -1,373 +0,0 @@
-_ = require 'underscore-plus'
-{Range} = require 'atom'
-
-{
- moveCursorLeft
- moveCursorRight
- limitNumber
- isEmptyRow
- setBufferRow
-} = require './utils'
-Operator = require('./base').getClass('Operator')
-
-# Operator which start 'insert-mode'
-# -------------------------
-# [NOTE]
-# Rule: Don't make any text mutation before calling `@selectTarget()`.
-class ActivateInsertMode extends Operator
- @extend()
- requireTarget: false
- flashTarget: false
- finalSubmode: null
- supportInsertionCount: true
-
- observeWillDeactivateMode: ->
- disposable = @vimState.modeManager.preemptWillDeactivateMode ({mode}) =>
- return unless mode is 'insert'
- disposable.dispose()
-
- @vimState.mark.set('^', @editor.getCursorBufferPosition()) # Last insert-mode position
- textByUserInput = ''
- if change = @getChangeSinceCheckpoint('insert')
- @lastChange = change
- @setMarkForChange(new Range(change.start, change.start.traverse(change.newExtent)))
- textByUserInput = change.newText
- @vimState.register.set('.', text: textByUserInput) # Last inserted text
-
- _.times @getInsertionCount(), =>
- text = @textByOperator + textByUserInput
- for selection in @editor.getSelections()
- selection.insertText(text, autoIndent: true)
-
- # This cursor state is restored on undo.
- # So cursor state has to be updated before next groupChangesSinceCheckpoint()
- if @getConfig('clearMultipleCursorsOnEscapeInsertMode')
- @vimState.clearSelections()
-
- # grouping changes for undo checkpoint need to come last
- if @getConfig('groupChangesWhenLeavingInsertMode')
- @groupChangesSinceBufferCheckpoint('undo')
-
- # When each mutaion's extent is not intersecting, muitiple changes are recorded
- # e.g
- # - Multicursors edit
- # - Cursor moved in insert-mode(e.g ctrl-f, ctrl-b)
- # But I don't care multiple changes just because I'm lazy(so not perfect implementation).
- # I only take care of one change happened at earliest(topCursor's change) position.
- # Thats' why I save topCursor's position to @topCursorPositionAtInsertionStart to compare traversal to deletionStart
- # Why I use topCursor's change? Just because it's easy to use first change returned by getChangeSinceCheckpoint().
- getChangeSinceCheckpoint: (purpose) ->
- checkpoint = @getBufferCheckpoint(purpose)
- @editor.buffer.getChangesSinceCheckpoint(checkpoint)[0]
-
- # [BUG-BUT-OK] Replaying text-deletion-operation is not compatible to pure Vim.
- # Pure Vim record all operation in insert-mode as keystroke level and can distinguish
- # character deleted by `Delete` or by `ctrl-u`.
- # But I can not and don't trying to minic this level of compatibility.
- # So basically deletion-done-in-one is expected to work well.
- replayLastChange: (selection) ->
- if @lastChange?
- {start, newExtent, oldExtent, newText} = @lastChange
- unless oldExtent.isZero()
- traversalToStartOfDelete = start.traversalFrom(@topCursorPositionAtInsertionStart)
- deletionStart = selection.cursor.getBufferPosition().traverse(traversalToStartOfDelete)
- deletionEnd = deletionStart.traverse(oldExtent)
- selection.setBufferRange([deletionStart, deletionEnd])
- else
- newText = ''
- selection.insertText(newText, autoIndent: true)
-
- # called when repeated
- # [FIXME] to use replayLastChange in repeatInsert overriding subclasss.
- repeatInsert: (selection, text) ->
- @replayLastChange(selection)
-
- getInsertionCount: ->
- @insertionCount ?= if @supportInsertionCount then @getCount(-1) else 0
- # Avoid freezing by acccidental big count(e.g. `5555555555555i`), See #560, #596
- limitNumber(@insertionCount, max: 100)
-
- investigateCursorPosition: ->
- [c1, c2, c3] = []
- c1 = @editor.getCursorBufferPosition()
-
- @onWillActivateMode ({mode, submode}) =>
- c2 = @editor.getCursorBufferPosition()
- console.info 'diff c1, c2', c1.toString(), c2.toString() unless c1.isEqual(c2)
-
- @onDidActivateMode ({mode, submode}) =>
- c3 = @editor.getCursorBufferPosition()
- if c2.row isnt c3.row
- console.warn 'dff c2, c3', c2.toString(), c3.toString()
-
- execute: ->
- if @repeated
- @flashTarget = @trackChange = true
-
- @startMutation =>
- @selectTarget() if @target?
- @mutateText?()
- for selection in @editor.getSelections()
- @repeatInsert(selection, @lastChange?.newText ? '')
- moveCursorLeft(selection.cursor)
- @mutationManager.setCheckpoint('did-finish')
-
- if @getConfig('clearMultipleCursorsOnEscapeInsertMode')
- @vimState.clearSelections()
-
- else
- @investigateCursorPosition() if @getConfig("debug")
-
- @normalizeSelectionsIfNecessary()
- @createBufferCheckpoint('undo')
- @selectTarget() if @target?
- @observeWillDeactivateMode()
-
- @mutateText?()
-
- if @getInsertionCount() > 0
- @textByOperator = @getChangeSinceCheckpoint('undo')?.newText ? ''
-
- @createBufferCheckpoint('insert')
- topCursor = @editor.getCursorsOrderedByBufferPosition()[0]
- @topCursorPositionAtInsertionStart = topCursor.getBufferPosition()
-
- # Skip normalization of blockwiseSelection.
- # Since want to keep multi-cursor and it's position in when shift to insert-mode.
- for blockwiseSelection in @getBlockwiseSelections()
- blockwiseSelection.skipNormalization()
- @activateMode('insert', @finalSubmode)
-
-class ActivateReplaceMode extends ActivateInsertMode
- @extend()
- finalSubmode: 'replace'
-
- repeatInsert: (selection, text) ->
- for char in text when (char isnt "\n")
- break if selection.cursor.isAtEndOfLine()
- selection.selectRight()
- selection.insertText(text, autoIndent: false)
-
-class InsertAfter extends ActivateInsertMode
- @extend()
- execute: ->
- moveCursorRight(cursor) for cursor in @editor.getCursors()
- super
-
-# key: 'g I' in all mode
-class InsertAtBeginningOfLine extends ActivateInsertMode
- @extend()
- execute: ->
- if @mode is 'visual' and @submode in ['characterwise', 'linewise']
- @editor.splitSelectionsIntoLines()
- @editor.moveToBeginningOfLine()
- super
-
-# key: normal 'A'
-class InsertAfterEndOfLine extends ActivateInsertMode
- @extend()
- execute: ->
- @editor.moveToEndOfLine()
- super
-
-# key: normal 'I'
-class InsertAtFirstCharacterOfLine extends ActivateInsertMode
- @extend()
- execute: ->
- @editor.moveToBeginningOfLine()
- @editor.moveToFirstCharacterOfLine()
- super
-
-class InsertAtLastInsert extends ActivateInsertMode
- @extend()
- execute: ->
- if (point = @vimState.mark.get('^'))
- @editor.setCursorBufferPosition(point)
- @editor.scrollToCursorPosition({center: true})
- super
-
-class InsertAboveWithNewline extends ActivateInsertMode
- @extend()
-
- initialize: ->
- if @getConfig('groupChangesWhenLeavingInsertMode')
- @originalCursorPositionMarker = @editor.markBufferPosition(@editor.getCursorBufferPosition())
-
- # This is for `o` and `O` operator.
- # On undo/redo put cursor at original point where user type `o` or `O`.
- groupChangesSinceBufferCheckpoint: ->
- lastCursor = @editor.getLastCursor()
- cursorPosition = lastCursor.getBufferPosition()
- lastCursor.setBufferPosition(@originalCursorPositionMarker.getHeadBufferPosition())
- @originalCursorPositionMarker.destroy()
-
- super
-
- lastCursor.setBufferPosition(cursorPosition)
-
- autoIndentEmptyRows: ->
- for cursor in @editor.getCursors()
- row = cursor.getBufferRow()
- @editor.autoIndentBufferRow(row) if isEmptyRow(@editor, row)
-
- mutateText: ->
- @editor.insertNewlineAbove()
- @autoIndentEmptyRows() if @editor.autoIndent
-
- repeatInsert: (selection, text) ->
- selection.insertText(text.trimLeft(), autoIndent: true)
-
-class InsertBelowWithNewline extends InsertAboveWithNewline
- @extend()
- mutateText: ->
- for cursor in @editor.getCursors() when cursorRow = cursor.getBufferRow()
- setBufferRow(cursor, @getFoldEndRowForRow(cursorRow))
-
- @editor.insertNewlineBelow()
- @autoIndentEmptyRows() if @editor.autoIndent
-
-# Advanced Insertion
-# -------------------------
-class InsertByTarget extends ActivateInsertMode
- @extend(false)
- requireTarget: true
- which: null # one of ['start', 'end', 'head', 'tail']
-
- initialize: ->
- # HACK
- # When g i is mapped to `insert-at-start-of-target`.
- # `g i 3 l` start insert at 3 column right position.
- # In this case, we don't want repeat insertion 3 times.
- # This @getCount() call cache number at the timing BEFORE '3' is specified.
- @getCount()
- super
-
- execute: ->
- @onDidSelectTarget =>
- # In vC/vL, when occurrence marker was NOT selected,
- # it behave's very specially
- # vC: `I` and `A` behaves as shoft hand of `ctrl-v I` and `ctrl-v A`.
- # vL: `I` and `A` place cursors at each selected lines of start( or end ) of non-white-space char.
- if not @occurrenceSelected and @mode is 'visual' and @submode in ['characterwise', 'linewise']
- for $selection in @swrap.getSelections(@editor)
- $selection.normalize()
- $selection.applyWise('blockwise')
-
- if @submode is 'linewise'
- for blockwiseSelection in @getBlockwiseSelections()
- blockwiseSelection.expandMemberSelectionsOverLineWithTrimRange()
-
- for $selection in @swrap.getSelections(@editor)
- $selection.setBufferPositionTo(@which)
- super
-
-# key: 'I', Used in 'visual-mode.characterwise', visual-mode.blockwise
-class InsertAtStartOfTarget extends InsertByTarget
- @extend()
- which: 'start'
-
-# key: 'A', Used in 'visual-mode.characterwise', 'visual-mode.blockwise'
-class InsertAtEndOfTarget extends InsertByTarget
- @extend()
- which: 'end'
-
-class InsertAtHeadOfTarget extends InsertByTarget
- @extend()
- which: 'head'
-
-class InsertAtStartOfOccurrence extends InsertAtStartOfTarget
- @extend()
- occurrence: true
-
-class InsertAtEndOfOccurrence extends InsertAtEndOfTarget
- @extend()
- occurrence: true
-
-class InsertAtHeadOfOccurrence extends InsertAtHeadOfTarget
- @extend()
- occurrence: true
-
-class InsertAtStartOfSubwordOccurrence extends InsertAtStartOfOccurrence
- @extend()
- occurrenceType: 'subword'
-
-class InsertAtEndOfSubwordOccurrence extends InsertAtEndOfOccurrence
- @extend()
- occurrenceType: 'subword'
-
-class InsertAtHeadOfSubwordOccurrence extends InsertAtHeadOfOccurrence
- @extend()
- occurrenceType: 'subword'
-
-class InsertAtStartOfSmartWord extends InsertByTarget
- @extend()
- which: 'start'
- target: "MoveToPreviousSmartWord"
-
-class InsertAtEndOfSmartWord extends InsertByTarget
- @extend()
- which: 'end'
- target: "MoveToEndOfSmartWord"
-
-class InsertAtPreviousFoldStart extends InsertByTarget
- @extend()
- @description: "Move to previous fold start then enter insert-mode"
- which: 'start'
- target: 'MoveToPreviousFoldStart'
-
-class InsertAtNextFoldStart extends InsertByTarget
- @extend()
- @description: "Move to next fold start then enter insert-mode"
- which: 'end'
- target: 'MoveToNextFoldStart'
-
-# -------------------------
-class Change extends ActivateInsertMode
- @extend()
- requireTarget: true
- trackChange: true
- supportInsertionCount: false
-
- mutateText: ->
- # Allways dynamically determine selection wise wthout consulting target.wise
- # Reason: when `c i {`, wise is 'characterwise', but actually selected range is 'linewise'
- # {
- # a
- # }
- isLinewiseTarget = @swrap.detectWise(@editor) is 'linewise'
- for selection in @editor.getSelections()
- @setTextToRegisterForSelection(selection) unless @getConfig('dontUpdateRegisterOnChangeOrSubstitute')
- if isLinewiseTarget
- selection.insertText("\n", autoIndent: true)
- selection.cursor.moveLeft()
- else
- selection.insertText('', autoIndent: true)
-
-class ChangeOccurrence extends Change
- @extend()
- @description: "Change all matching word within target range"
- occurrence: true
-
-class Substitute extends Change
- @extend()
- target: 'MoveRight'
-
-class SubstituteLine extends Change
- @extend()
- wise: 'linewise' # [FIXME] to re-override target.wise in visual-mode
- target: 'MoveToRelativeLine'
-
-# alias
-class ChangeLine extends SubstituteLine
- @extend()
-
-class ChangeToLastCharacterOfLine extends Change
- @extend()
- target: 'MoveToLastCharacterOfLine'
-
- execute: ->
- if @target.wise is 'blockwise'
- @onDidSelectTarget =>
- for blockwiseSelection in @getBlockwiseSelections()
- blockwiseSelection.extendMemberSelectionsToEndOfLine()
- super
diff --git a/lib/operator-insert.js b/lib/operator-insert.js
new file mode 100644
index 000000000..d7cc4531d
--- /dev/null
+++ b/lib/operator-insert.js
@@ -0,0 +1,446 @@
+"use babel"
+
+const _ = require("underscore-plus")
+const {Range} = require("atom")
+
+const {moveCursorLeft, moveCursorRight, limitNumber, isEmptyRow, setBufferRow} = require("./utils")
+const Operator = require("./base").getClass("Operator")
+
+// Operator which start 'insert-mode'
+// -------------------------
+// [NOTE]
+// Rule: Don't make any text mutation before calling `@selectTarget()`.
+class ActivateInsertMode extends Operator {
+ requireTarget = false
+ flashTarget = false
+ finalSubmode = null
+ supportInsertionCount = true
+
+ observeWillDeactivateMode() {
+ let disposable = this.vimState.modeManager.preemptWillDeactivateMode(({mode}) => {
+ if (mode !== "insert") return
+ disposable.dispose()
+
+ this.vimState.mark.set("^", this.editor.getCursorBufferPosition()) // Last insert-mode position
+ let textByUserInput = ""
+ const change = this.getChangeSinceCheckpoint("insert")
+ if (change) {
+ this.lastChange = change
+ this.setMarkForChange(new Range(change.start, change.start.traverse(change.newExtent)))
+ textByUserInput = change.newText
+ }
+ this.vimState.register.set(".", {text: textByUserInput}) // Last inserted text
+
+ _.times(this.getInsertionCount(), () => {
+ const textToInsert = this.textByOperator + textByUserInput
+ for (const selection of this.editor.getSelections()) {
+ selection.insertText(textToInsert, {autoIndent: true})
+ }
+ })
+
+ // This cursor state is restored on undo.
+ // So cursor state has to be updated before next groupChangesSinceCheckpoint()
+ if (this.getConfig("clearMultipleCursorsOnEscapeInsertMode")) {
+ this.vimState.clearSelections()
+ }
+
+ // grouping changes for undo checkpoint need to come last
+ if (this.getConfig("groupChangesWhenLeavingInsertMode")) {
+ return this.groupChangesSinceBufferCheckpoint("undo")
+ }
+ })
+ }
+
+ // When each mutaion's extent is not intersecting, muitiple changes are recorded
+ // e.g
+ // - Multicursors edit
+ // - Cursor moved in insert-mode(e.g ctrl-f, ctrl-b)
+ // But I don't care multiple changes just because I'm lazy(so not perfect implementation).
+ // I only take care of one change happened at earliest(topCursor's change) position.
+ // Thats' why I save topCursor's position to @topCursorPositionAtInsertionStart to compare traversal to deletionStart
+ // Why I use topCursor's change? Just because it's easy to use first change returned by getChangeSinceCheckpoint().
+ getChangeSinceCheckpoint(purpose) {
+ const checkpoint = this.getBufferCheckpoint(purpose)
+ return this.editor.buffer.getChangesSinceCheckpoint(checkpoint)[0]
+ }
+
+ // [BUG-BUT-OK] Replaying text-deletion-operation is not compatible to pure Vim.
+ // Pure Vim record all operation in insert-mode as keystroke level and can distinguish
+ // character deleted by `Delete` or by `ctrl-u`.
+ // But I can not and don't trying to minic this level of compatibility.
+ // So basically deletion-done-in-one is expected to work well.
+ replayLastChange(selection) {
+ let textToInsert
+ if (this.lastChange != null) {
+ const {start, newExtent, oldExtent, newText} = this.lastChange
+ if (!oldExtent.isZero()) {
+ const traversalToStartOfDelete = start.traversalFrom(this.topCursorPositionAtInsertionStart)
+ const deletionStart = selection.cursor.getBufferPosition().traverse(traversalToStartOfDelete)
+ const deletionEnd = deletionStart.traverse(oldExtent)
+ selection.setBufferRange([deletionStart, deletionEnd])
+ }
+ textToInsert = newText
+ } else {
+ textToInsert = ""
+ }
+ selection.insertText(textToInsert, {autoIndent: true})
+ }
+
+ // called when repeated
+ // [FIXME] to use replayLastChange in repeatInsert overriding subclasss.
+ repeatInsert(selection, text) {
+ this.replayLastChange(selection)
+ }
+
+ getInsertionCount() {
+ if (this.insertionCount == null) {
+ this.insertionCount = this.supportInsertionCount ? this.getCount(-1) : 0
+ }
+ // Avoid freezing by acccidental big count(e.g. `5555555555555i`), See #560, #596
+ return limitNumber(this.insertionCount, {max: 100})
+ }
+
+ execute() {
+ if (this.repeated) {
+ this.flashTarget = this.trackChange = true
+
+ this.startMutation(() => {
+ if (this.target) this.selectTarget()
+ if (this.mutateText) this.mutateText()
+
+ for (const selection of this.editor.getSelections()) {
+ const textToInsert = (this.lastChange && this.lastChange.newText) || ""
+ this.repeatInsert(selection, textToInsert)
+ moveCursorLeft(selection.cursor)
+ }
+ this.mutationManager.setCheckpoint("did-finish")
+ })
+
+ if (this.getConfig("clearMultipleCursorsOnEscapeInsertMode")) this.vimState.clearSelections()
+ } else {
+ this.normalizeSelectionsIfNecessary()
+ this.createBufferCheckpoint("undo")
+ if (this.target) this.selectTarget()
+ this.observeWillDeactivateMode()
+ if (this.mutateText) this.mutateText()
+
+ if (this.getInsertionCount() > 0) {
+ const change = this.getChangeSinceCheckpoint("undo")
+ this.textByOperator = (change && change.newText) || ""
+ }
+
+ this.createBufferCheckpoint("insert")
+ const topCursor = this.editor.getCursorsOrderedByBufferPosition()[0]
+ this.topCursorPositionAtInsertionStart = topCursor.getBufferPosition()
+
+ // Skip normalization of blockwiseSelection.
+ // Since want to keep multi-cursor and it's position in when shift to insert-mode.
+ for (const blockwiseSelection of this.getBlockwiseSelections()) {
+ blockwiseSelection.skipNormalization()
+ }
+ this.activateMode("insert", this.finalSubmode)
+ }
+ }
+}
+ActivateInsertMode.register()
+
+class ActivateReplaceMode extends ActivateInsertMode {
+ finalSubmode = "replace"
+
+ repeatInsert(selection, text) {
+ for (const char of text) {
+ if (char === "\n") continue
+ if (selection.cursor.isAtEndOfLine()) break
+ selection.selectRight()
+ }
+ selection.insertText(text, {autoIndent: false})
+ }
+}
+ActivateReplaceMode.register()
+
+class InsertAfter extends ActivateInsertMode {
+ execute() {
+ for (const cursor of this.editor.getCursors()) {
+ moveCursorRight(cursor)
+ }
+ super.execute()
+ }
+}
+InsertAfter.register()
+
+// key: 'g I' in all mode
+class InsertAtBeginningOfLine extends ActivateInsertMode {
+ execute() {
+ if (this.mode === "visual" && this.submode !== "blockwise") {
+ this.editor.splitSelectionsIntoLines()
+ }
+ this.editor.moveToBeginningOfLine()
+ super.execute()
+ }
+}
+InsertAtBeginningOfLine.register()
+
+// key: normal 'A'
+class InsertAfterEndOfLine extends ActivateInsertMode {
+ execute() {
+ this.editor.moveToEndOfLine()
+ super.execute()
+ }
+}
+InsertAfterEndOfLine.register()
+
+// key: normal 'I'
+class InsertAtFirstCharacterOfLine extends ActivateInsertMode {
+ execute() {
+ this.editor.moveToBeginningOfLine()
+ this.editor.moveToFirstCharacterOfLine()
+ super.execute()
+ }
+}
+InsertAtFirstCharacterOfLine.register()
+
+class InsertAtLastInsert extends ActivateInsertMode {
+ execute() {
+ const point = this.vimState.mark.get("^")
+ if (point) {
+ this.editor.setCursorBufferPosition(point)
+ this.editor.scrollToCursorPosition({center: true})
+ }
+ super.execute()
+ }
+}
+InsertAtLastInsert.register()
+
+class InsertAboveWithNewline extends ActivateInsertMode {
+ initialize() {
+ if (this.getConfig("groupChangesWhenLeavingInsertMode")) {
+ this.originalCursorPositionMarker = this.editor.markBufferPosition(this.editor.getCursorBufferPosition())
+ }
+ return super.initialize()
+ }
+
+ // This is for `o` and `O` operator.
+ // On undo/redo put cursor at original point where user type `o` or `O`.
+ groupChangesSinceBufferCheckpoint(purpose) {
+ const lastCursor = this.editor.getLastCursor()
+ const cursorPosition = lastCursor.getBufferPosition()
+ lastCursor.setBufferPosition(this.originalCursorPositionMarker.getHeadBufferPosition())
+ this.originalCursorPositionMarker.destroy()
+
+ super.groupChangesSinceBufferCheckpoint(purpose)
+
+ lastCursor.setBufferPosition(cursorPosition)
+ }
+
+ autoIndentEmptyRows() {
+ for (const cursor of this.editor.getCursors()) {
+ const row = cursor.getBufferRow()
+ if (isEmptyRow(this.editor, row)) {
+ this.editor.autoIndentBufferRow(row)
+ }
+ }
+ }
+
+ mutateText() {
+ this.editor.insertNewlineAbove()
+ if (this.editor.autoIndent) {
+ this.autoIndentEmptyRows()
+ }
+ }
+
+ repeatInsert(selection, text) {
+ selection.insertText(text.trimLeft(), {autoIndent: true})
+ }
+}
+InsertAboveWithNewline.register()
+
+class InsertBelowWithNewline extends InsertAboveWithNewline {
+ mutateText() {
+ for (const cursor of this.editor.getCursors()) {
+ setBufferRow(cursor, this.getFoldEndRowForRow(cursor.getBufferRow()))
+ }
+
+ this.editor.insertNewlineBelow()
+ if (this.editor.autoIndent) this.autoIndentEmptyRows()
+ }
+}
+InsertBelowWithNewline.register()
+
+// Advanced Insertion
+// -------------------------
+class InsertByTarget extends ActivateInsertMode {
+ requireTarget = true
+ which = null // one of ['start', 'end', 'head', 'tail']
+
+ initialize() {
+ // HACK
+ // When g i is mapped to `insert-at-start-of-target`.
+ // `g i 3 l` start insert at 3 column right position.
+ // In this case, we don't want repeat insertion 3 times.
+ // This @getCount() call cache number at the timing BEFORE '3' is specified.
+ this.getCount()
+ return super.initialize()
+ }
+
+ execute() {
+ this.onDidSelectTarget(() => {
+ // In vC/vL, when occurrence marker was NOT selected,
+ // it behave's very specially
+ // vC: `I` and `A` behaves as shoft hand of `ctrl-v I` and `ctrl-v A`.
+ // vL: `I` and `A` place cursors at each selected lines of start( or end ) of non-white-space char.
+ if (!this.occurrenceSelected && this.mode === "visual" && this.submode !== "blockwise") {
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ $selection.normalize()
+ $selection.applyWise("blockwise")
+ }
+
+ if (this.submode === "linewise") {
+ for (const blockwiseSelection of this.getBlockwiseSelections()) {
+ blockwiseSelection.expandMemberSelectionsOverLineWithTrimRange()
+ }
+ }
+ }
+
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ $selection.setBufferPositionTo(this.which)
+ }
+ })
+ super.execute()
+ }
+}
+InsertByTarget.register(false)
+
+// key: 'I', Used in 'visual-mode.characterwise', visual-mode.blockwise
+class InsertAtStartOfTarget extends InsertByTarget {
+ which = "start"
+}
+InsertAtStartOfTarget.register()
+
+// key: 'A', Used in 'visual-mode.characterwise', 'visual-mode.blockwise'
+class InsertAtEndOfTarget extends InsertByTarget {
+ which = "end"
+}
+InsertAtEndOfTarget.register()
+
+class InsertAtHeadOfTarget extends InsertByTarget {
+ which = "head"
+}
+InsertAtHeadOfTarget.register()
+
+class InsertAtStartOfOccurrence extends InsertAtStartOfTarget {
+ occurrence = true
+}
+InsertAtStartOfOccurrence.register()
+
+class InsertAtEndOfOccurrence extends InsertAtEndOfTarget {
+ occurrence = true
+}
+InsertAtEndOfOccurrence.register()
+
+class InsertAtHeadOfOccurrence extends InsertAtHeadOfTarget {
+ occurrence = true
+}
+InsertAtHeadOfOccurrence.register()
+
+class InsertAtStartOfSubwordOccurrence extends InsertAtStartOfOccurrence {
+ occurrenceType = "subword"
+}
+InsertAtStartOfSubwordOccurrence.register()
+
+class InsertAtEndOfSubwordOccurrence extends InsertAtEndOfOccurrence {
+ occurrenceType = "subword"
+}
+InsertAtEndOfSubwordOccurrence.register()
+
+class InsertAtHeadOfSubwordOccurrence extends InsertAtHeadOfOccurrence {
+ occurrenceType = "subword"
+}
+InsertAtHeadOfSubwordOccurrence.register()
+
+class InsertAtStartOfSmartWord extends InsertByTarget {
+ which = "start"
+ target = "MoveToPreviousSmartWord"
+}
+InsertAtStartOfSmartWord.register()
+
+class InsertAtEndOfSmartWord extends InsertByTarget {
+ which = "end"
+ target = "MoveToEndOfSmartWord"
+}
+InsertAtEndOfSmartWord.register()
+
+class InsertAtPreviousFoldStart extends InsertByTarget {
+ which = "start"
+ target = "MoveToPreviousFoldStart"
+}
+InsertAtPreviousFoldStart.register()
+
+class InsertAtNextFoldStart extends InsertByTarget {
+ which = "end"
+ target = "MoveToNextFoldStart"
+}
+InsertAtNextFoldStart.register()
+
+// -------------------------
+class Change extends ActivateInsertMode {
+ requireTarget = true
+ trackChange = true
+ supportInsertionCount = false
+
+ mutateText() {
+ // Allways dynamically determine selection wise wthout consulting target.wise
+ // Reason: when `c i {`, wise is 'characterwise', but actually selected range is 'linewise'
+ // {
+ // a
+ // }
+ const isLinewiseTarget = this.swrap.detectWise(this.editor) === "linewise"
+ for (const selection of this.editor.getSelections()) {
+ if (!this.getConfig("dontUpdateRegisterOnChangeOrSubstitute")) {
+ this.setTextToRegisterForSelection(selection)
+ }
+ if (isLinewiseTarget) {
+ selection.insertText("\n", {autoIndent: true})
+ selection.cursor.moveLeft()
+ } else {
+ selection.insertText("", {autoIndent: true})
+ }
+ }
+ }
+}
+Change.register()
+
+class ChangeOccurrence extends Change {
+ occurrence = true
+}
+ChangeOccurrence.register()
+
+class Substitute extends Change {
+ target = "MoveRight"
+}
+Substitute.register()
+
+class SubstituteLine extends Change {
+ wise = "linewise" // [FIXME] to re-override target.wise in visual-mode
+ target = "MoveToRelativeLine"
+}
+SubstituteLine.register()
+
+// alias
+class ChangeLine extends SubstituteLine {}
+ChangeLine.register()
+
+class ChangeToLastCharacterOfLine extends Change {
+ target = "MoveToLastCharacterOfLine"
+
+ execute() {
+ this.onDidSelectTarget(() => {
+ if (this.target.wise === "blockwise") {
+ for (const blockwiseSelection of this.getBlockwiseSelections()) {
+ blockwiseSelection.extendMemberSelectionsToEndOfLine()
+ }
+ }
+ })
+ super.execute()
+ }
+}
+ChangeToLastCharacterOfLine.register()
diff --git a/lib/operator-transform-string.coffee b/lib/operator-transform-string.coffee
deleted file mode 100644
index 72f6753a0..000000000
--- a/lib/operator-transform-string.coffee
+++ /dev/null
@@ -1,778 +0,0 @@
-_ = require 'underscore-plus'
-{BufferedProcess, Range} = require 'atom'
-
-{
- isSingleLineText
- isLinewiseRange
- limitNumber
- toggleCaseForCharacter
- splitTextByNewLine
- splitArguments
- getIndentLevelForBufferRow
- adjustIndentWithKeepingLayout
-} = require './utils'
-Base = require './base'
-Operator = Base.getClass('Operator')
-
-# TransformString
-# ================================
-class TransformString extends Operator
- @extend(false)
- trackChange: true
- stayOptionName: 'stayOnTransformString'
- autoIndent: false
- autoIndentNewline: false
- autoIndentAfterInsertText: false
- @stringTransformers: []
-
- @registerToSelectList: ->
- @stringTransformers.push(this)
-
- mutateSelection: (selection) ->
- if text = @getNewText(selection.getText(), selection)
- if @autoIndentAfterInsertText
- startRow = selection.getBufferRange().start.row
- startRowIndentLevel = getIndentLevelForBufferRow(@editor, startRow)
- range = selection.insertText(text, {@autoIndent, @autoIndentNewline})
-
- if @autoIndentAfterInsertText
- # Currently used by SplitArguments and Surround( linewise target only )
- range = range.translate([0, 0], [-1, 0]) if @target.isLinewise()
- @editor.setIndentationForBufferRow(range.start.row, startRowIndentLevel)
- @editor.setIndentationForBufferRow(range.end.row, startRowIndentLevel)
- # Adjust inner range, end.row is already( if needed ) translated so no need to re-translate.
- adjustIndentWithKeepingLayout(@editor, range.translate([1, 0], [0, 0]))
-
-class ToggleCase extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`Hello World` -> `hELLO wORLD`"
- displayName: 'Toggle ~'
-
- getNewText: (text) ->
- text.replace(/./g, toggleCaseForCharacter)
-
-class ToggleCaseAndMoveRight extends ToggleCase
- @extend()
- flashTarget: false
- restorePositions: false
- target: 'MoveRight'
-
-class UpperCase extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`Hello World` -> `HELLO WORLD`"
- displayName: 'Upper'
- getNewText: (text) ->
- text.toUpperCase()
-
-class LowerCase extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`Hello World` -> `hello world`"
- displayName: 'Lower'
- getNewText: (text) ->
- text.toLowerCase()
-
-# Replace
-# -------------------------
-class Replace extends TransformString
- @extend()
- @registerToSelectList()
- flashCheckpoint: 'did-select-occurrence'
- input: null
- requireInput: true
- autoIndentNewline: true
- supportEarlySelect: true
-
- initialize: ->
- @onDidSelectTarget =>
- @focusInput(hideCursor: true)
- super
-
- getNewText: (text) ->
- if @target.is('MoveRightBufferColumn') and text.length isnt @getCount()
- return
-
- input = @input or "\n"
- if input is "\n"
- @restorePositions = false
- text.replace(/./g, input)
-
-class ReplaceCharacter extends Replace
- @extend()
- target: "MoveRightBufferColumn"
-
-# -------------------------
-# DUP meaning with SplitString need consolidate.
-class SplitByCharacter extends TransformString
- @extend()
- @registerToSelectList()
- getNewText: (text) ->
- text.split('').join(' ')
-
-class CamelCase extends TransformString
- @extend()
- @registerToSelectList()
- displayName: 'Camelize'
- @description: "`hello-world` -> `helloWorld`"
- getNewText: (text) ->
- _.camelize(text)
-
-class SnakeCase extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`HelloWorld` -> `hello_world`"
- displayName: 'Underscore _'
- getNewText: (text) ->
- _.underscore(text)
-
-class PascalCase extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`hello_world` -> `HelloWorld`"
- displayName: 'Pascalize'
- getNewText: (text) ->
- _.capitalize(_.camelize(text))
-
-class DashCase extends TransformString
- @extend()
- @registerToSelectList()
- displayName: 'Dasherize -'
- @description: "HelloWorld -> hello-world"
- getNewText: (text) ->
- _.dasherize(text)
-
-class TitleCase extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`HelloWorld` -> `Hello World`"
- displayName: 'Titlize'
- getNewText: (text) ->
- _.humanizeEventName(_.dasherize(text))
-
-class EncodeUriComponent extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`Hello World` -> `Hello%20World`"
- displayName: 'Encode URI Component %'
- getNewText: (text) ->
- encodeURIComponent(text)
-
-class DecodeUriComponent extends TransformString
- @extend()
- @registerToSelectList()
- @description: "`Hello%20World` -> `Hello World`"
- displayName: 'Decode URI Component %%'
- getNewText: (text) ->
- decodeURIComponent(text)
-
-class TrimString extends TransformString
- @extend()
- @registerToSelectList()
- @description: "` hello ` -> `hello`"
- displayName: 'Trim string'
- getNewText: (text) ->
- text.trim()
-
-class CompactSpaces extends TransformString
- @extend()
- @registerToSelectList()
- @description: "` a b c` -> `a b c`"
- displayName: 'Compact space'
- getNewText: (text) ->
- if text.match(/^[ ]+$/)
- ' '
- else
- # Don't compact for leading and trailing white spaces.
- text.replace /^(\s*)(.*?)(\s*)$/gm, (m, leading, middle, trailing) ->
- leading + middle.split(/[ \t]+/).join(' ') + trailing
-
-class RemoveLeadingWhiteSpaces extends TransformString
- @extend()
- @registerToSelectList()
- wise: 'linewise'
- @description: "` a b c` -> `a b c`"
- getNewText: (text, selection) ->
- trimLeft = (text) -> text.trimLeft()
- splitTextByNewLine(text).map(trimLeft).join("\n") + "\n"
-
-class ConvertToSoftTab extends TransformString
- @extend()
- @registerToSelectList()
- displayName: 'Soft Tab'
- wise: 'linewise'
-
- mutateSelection: (selection) ->
- @scanForward /\t/g, {scanRange: selection.getBufferRange()}, ({range, replace}) =>
- # Replace \t to spaces which length is vary depending on tabStop and tabLenght
- # So we directly consult it's screen representing length.
- length = @editor.screenRangeForBufferRange(range).getExtent().column
- replace(" ".repeat(length))
-
-class ConvertToHardTab extends TransformString
- @extend()
- @registerToSelectList()
- displayName: 'Hard Tab'
-
- mutateSelection: (selection) ->
- tabLength = @editor.getTabLength()
- @scanForward /[ \t]+/g, {scanRange: selection.getBufferRange()}, ({range, replace}) =>
- {start, end} = @editor.screenRangeForBufferRange(range)
- startColumn = start.column
- endColumn = end.column
-
- # We can't naively replace spaces to tab, we have to consider valid tabStop column
- # If nextTabStop column exceeds replacable range, we pad with spaces.
- newText = ''
- loop
- remainder = startColumn %% tabLength
- nextTabStop = startColumn + (if remainder is 0 then tabLength else remainder)
- if nextTabStop > endColumn
- newText += " ".repeat(endColumn - startColumn)
- else
- newText += "\t"
- startColumn = nextTabStop
- break if startColumn >= endColumn
-
- replace(newText)
-
-# -------------------------
-class TransformStringByExternalCommand extends TransformString
- @extend(false)
- autoIndent: true
- command: '' # e.g. command: 'sort'
- args: [] # e.g args: ['-rn']
- stdoutBySelection: null
-
- execute: ->
- @normalizeSelectionsIfNecessary()
- if @selectTarget()
- new Promise (resolve) =>
- @collect(resolve)
- .then =>
- for selection in @editor.getSelections()
- text = @getNewText(selection.getText(), selection)
- selection.insertText(text, {@autoIndent})
- @restoreCursorPositionsIfNecessary()
- @activateMode("normal")
-
- collect: (resolve) ->
- @stdoutBySelection = new Map
- processRunning = processFinished = 0
- for selection in @editor.getSelections()
- {command, args} = @getCommand(selection) ? {}
- return unless (command? and args?)
- processRunning++
- do (selection) =>
- stdin = @getStdin(selection)
- stdout = (output) =>
- @stdoutBySelection.set(selection, output)
- exit = (code) ->
- processFinished++
- resolve() if (processRunning is processFinished)
- @runExternalCommand {command, args, stdout, exit, stdin}
-
- runExternalCommand: (options) ->
- stdin = options.stdin
- delete options.stdin
- bufferedProcess = new BufferedProcess(options)
- bufferedProcess.onWillThrowError ({error, handle}) =>
- # Suppress command not found error intentionally.
- if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
- commandName = @constructor.getCommandName()
- console.log "#{commandName}: Failed to spawn command #{error.path}."
- handle()
- @cancelOperation()
-
- if stdin
- bufferedProcess.process.stdin.write(stdin)
- bufferedProcess.process.stdin.end()
-
- getNewText: (text, selection) ->
- @getStdout(selection) ? text
-
- # For easily extend by vmp plugin.
- getCommand: (selection) -> {@command, @args}
- getStdin: (selection) -> selection.getText()
- getStdout: (selection) -> @stdoutBySelection.get(selection)
-
-# -------------------------
-class TransformStringBySelectList extends TransformString
- @extend()
- @description: "Interactively choose string transformation operator from select-list"
- @selectListItems: null
- requireInput: true
-
- getItems: ->
- @constructor.selectListItems ?= @constructor.stringTransformers.map (klass) ->
- if klass::hasOwnProperty('displayName')
- displayName = klass::displayName
- else
- displayName = _.humanizeEventName(_.dasherize(klass.name))
- {name: klass, displayName}
-
- initialize: ->
- super
-
- @vimState.onDidConfirmSelectList (item) =>
- transformer = item.name
- @target = transformer::target if transformer::target?
- @vimState.reset()
- if @target?
- @vimState.operationStack.run(transformer, {@target})
- else
- @vimState.operationStack.run(transformer)
-
- @focusSelectList(items: @getItems())
-
- execute: ->
- # NEVER be executed since operationStack is replaced with selected transformer
- throw new Error("#{@name} should not be executed")
-
-class TransformWordBySelectList extends TransformStringBySelectList
- @extend()
- target: "InnerWord"
-
-class TransformSmartWordBySelectList extends TransformStringBySelectList
- @extend()
- @description: "Transform InnerSmartWord by `transform-string-by-select-list`"
- target: "InnerSmartWord"
-
-# -------------------------
-class ReplaceWithRegister extends TransformString
- @extend()
- @description: "Replace target with specified register value"
- flashType: 'operator-long'
-
- initialize: ->
- @vimState.sequentialPasteManager.onInitialize(this)
-
- execute: ->
- @sequentialPaste = @vimState.sequentialPasteManager.onExecute(this)
-
- super
-
- for selection in @editor.getSelections()
- range = @mutationManager.getMutatedBufferRangeForSelection(selection)
- @vimState.sequentialPasteManager.savePastedRangeForSelection(selection, range)
-
- getNewText: (text, selection) ->
- @vimState.register.get(null, selection, @sequentialPaste)?.text ? ""
-
-# Save text to register before replace
-class SwapWithRegister extends TransformString
- @extend()
- @description: "Swap register value with target"
- getNewText: (text, selection) ->
- newText = @vimState.register.getText()
- @setTextToRegister(text, selection)
- newText
-
-# Indent < TransformString
-# -------------------------
-class Indent extends TransformString
- @extend()
- stayByMarker: true
- setToFirstCharacterOnLinewise: true
- wise: 'linewise'
-
- mutateSelection: (selection) ->
- # Need count times indentation in visual-mode and its repeat(`.`).
- if @target.is('CurrentSelection')
- oldText = null
- # limit to 100 to avoid freezing by accidental big number.
- count = limitNumber(@getCount(), max: 100)
- @countTimes count, ({stop}) =>
- oldText = selection.getText()
- @indent(selection)
- stop() if selection.getText() is oldText
- else
- @indent(selection)
-
- indent: (selection) ->
- selection.indentSelectedRows()
-
-class Outdent extends Indent
- @extend()
- indent: (selection) ->
- selection.outdentSelectedRows()
-
-class AutoIndent extends Indent
- @extend()
- indent: (selection) ->
- selection.autoIndentSelectedRows()
-
-class ToggleLineComments extends TransformString
- @extend()
- flashTarget: false
- stayByMarker: true
- wise: 'linewise'
-
- mutateSelection: (selection) ->
- selection.toggleLineComments()
-
-class Reflow extends TransformString
- @extend()
- mutateSelection: (selection) ->
- atom.commands.dispatch(@editorElement, 'autoflow:reflow-selection')
-
-class ReflowWithStay extends Reflow
- @extend()
- stayAtSamePosition: true
-
-# Surround < TransformString
-# -------------------------
-class SurroundBase extends TransformString
- @extend(false)
- pairs: [
- ['[', ']']
- ['(', ')']
- ['{', '}']
- ['<', '>']
- ]
- pairsByAlias: {
- b: ['(', ')']
- B: ['{', '}']
- r: ['[', ']']
- a: ['<', '>']
- }
-
- pairCharsAllowForwarding: '[](){}'
- input: null
- requireInput: true
- supportEarlySelect: true # Experimental
-
- focusInputForSurroundChar: ->
- @focusInput(hideCursor: true)
-
- focusInputForTargetPairChar: ->
- @focusInput(onConfirm: (char) => @onConfirmTargetPairChar(char))
-
- getPair: (char) ->
- pair = @pairsByAlias[char]
- pair ?= _.detect(@pairs, (pair) -> char in pair)
- pair ?= [char, char]
- pair
-
- surround: (text, char, options={}) ->
- keepLayout = options.keepLayout ? false
- [open, close] = @getPair(char)
- if (not keepLayout) and text.endsWith("\n")
- @autoIndentAfterInsertText = true
- open += "\n"
- close += "\n"
-
- if char in @getConfig('charactersToAddSpaceOnSurround') and isSingleLineText(text)
- text = ' ' + text + ' '
-
- open + text + close
-
- deleteSurround: (text) ->
- [open, innerText..., close] = text
- innerText = innerText.join('')
- if isSingleLineText(text) and (open isnt close)
- innerText.trim()
- else
- innerText
-
- onConfirmTargetPairChar: (char) ->
- @setTarget @new('APair', pair: @getPair(char))
-
-class Surround extends SurroundBase
- @extend()
- @description: "Surround target by specified character like `(`, `[`, `\"`"
-
- initialize: ->
- @onDidSelectTarget(@focusInputForSurroundChar.bind(this))
- super
-
- getNewText: (text) ->
- @surround(text, @input)
-
-class SurroundWord extends Surround
- @extend()
- @description: "Surround **word**"
- target: 'InnerWord'
-
-class SurroundSmartWord extends Surround
- @extend()
- @description: "Surround **smart-word**"
- target: 'InnerSmartWord'
-
-class MapSurround extends Surround
- @extend()
- @description: "Surround each word(`/\w+/`) within target"
- occurrence: true
- patternForOccurrence: /\w+/g
-
-# Delete Surround
-# -------------------------
-class DeleteSurround extends SurroundBase
- @extend()
- @description: "Delete specified surround character like `(`, `[`, `\"`"
-
- initialize: ->
- @focusInputForTargetPairChar() unless @target?
- super
-
- onConfirmTargetPairChar: (char) ->
- super
- @input = char
- @processOperation()
-
- getNewText: (text) ->
- @deleteSurround(text)
-
-class DeleteSurroundAnyPair extends DeleteSurround
- @extend()
- @description: "Delete surround character by auto-detect paired char from cursor enclosed pair"
- target: 'AAnyPair'
- requireInput: false
-
-class DeleteSurroundAnyPairAllowForwarding extends DeleteSurroundAnyPair
- @extend()
- @description: "Delete surround character by auto-detect paired char from cursor enclosed pair and forwarding pair within same line"
- target: 'AAnyPairAllowForwarding'
-
-# Change Surround
-# -------------------------
-class ChangeSurround extends SurroundBase
- @extend()
- @description: "Change surround character, specify both from and to pair char"
-
- showDeleteCharOnHover: ->
- hoverPoint = @mutationManager.getInitialPointForSelection(@editor.getLastSelection())
- char = @editor.getSelectedText()[0]
- @vimState.hover.set(char, hoverPoint)
-
- initialize: ->
- if @target?
- @onDidFailSelectTarget(@abort.bind(this))
- else
- @onDidFailSelectTarget(@cancelOperation.bind(this))
- @focusInputForTargetPairChar()
- super
-
- @onDidSelectTarget =>
- @showDeleteCharOnHover()
- @focusInputForSurroundChar()
-
- getNewText: (text) ->
- innerText = @deleteSurround(text)
- @surround(innerText, @input, keepLayout: true)
-
-class ChangeSurroundAnyPair extends ChangeSurround
- @extend()
- @description: "Change surround character, from char is auto-detected"
- target: "AAnyPair"
-
-class ChangeSurroundAnyPairAllowForwarding extends ChangeSurroundAnyPair
- @extend()
- @description: "Change surround character, from char is auto-detected from enclosed and forwarding area"
- target: "AAnyPairAllowForwarding"
-
-# -------------------------
-# FIXME
-# Currently native editor.joinLines() is better for cursor position setting
-# So I use native methods for a meanwhile.
-class Join extends TransformString
- @extend()
- target: "MoveToRelativeLine"
- flashTarget: false
- restorePositions: false
-
- mutateSelection: (selection) ->
- range = selection.getBufferRange()
-
- # When cursor is at last BUFFER row, it select last-buffer-row, then
- # joinning result in "clear last-buffer-row text".
- # I believe this is BUG of upstream atom-core. guard this situation here
- unless (range.isSingleLine() and range.end.row is @editor.getLastBufferRow())
- if isLinewiseRange(range)
- selection.setBufferRange(range.translate([0, 0], [-1, Infinity]))
- selection.joinLines()
- end = selection.getBufferRange().end
- selection.cursor.setBufferPosition(end.translate([0, -1]))
-
-class JoinBase extends TransformString
- @extend(false)
- wise: 'linewise'
- trim: false
- target: "MoveToRelativeLineMinimumOne"
-
- initialize: ->
- @focusInput(charsMax: 10) if @requireInput
- super
-
- getNewText: (text) ->
- if @trim
- pattern = /\r?\n[ \t]*/g
- else
- pattern = /\r?\n/g
- text.trimRight().replace(pattern, @input) + "\n"
-
-class JoinWithKeepingSpace extends JoinBase
- @extend()
- @registerToSelectList()
- input: ''
-
-class JoinByInput extends JoinBase
- @extend()
- @registerToSelectList()
- @description: "Transform multi-line to single-line by with specified separator character"
- requireInput: true
- trim: true
-
-class JoinByInputWithKeepingSpace extends JoinByInput
- @extend()
- @registerToSelectList()
- @description: "Join lines without padding space between each line"
- trim: false
-
-# -------------------------
-# String suffix in name is to avoid confusion with 'split' window.
-class SplitString extends TransformString
- @extend()
- @registerToSelectList()
- @description: "Split single-line into multi-line by splitting specified separator chars"
- requireInput: true
- input: null
- target: "MoveToRelativeLine"
- keepSplitter: false
-
- initialize: ->
- @onDidSetTarget =>
- @focusInput(charsMax: 10)
- super
-
- getNewText: (text) ->
- input = @input or "\\n"
- regex = ///#{_.escapeRegExp(input)}///g
- if @keepSplitter
- lineSeparator = @input + "\n"
- else
- lineSeparator = "\n"
- text.replace(regex, lineSeparator)
-
-class SplitStringWithKeepingSplitter extends SplitString
- @extend()
- @registerToSelectList()
- keepSplitter: true
-
-class SplitArguments extends TransformString
- @extend()
- @registerToSelectList()
- keepSeparator: true
- autoIndentAfterInsertText: true
-
- getNewText: (text) ->
- allTokens = splitArguments(text.trim())
- newText = ''
- while allTokens.length
- {text, type} = allTokens.shift()
- if type is 'separator'
- if @keepSeparator
- text = text.trim() + "\n"
- else
- text = "\n"
- newText += text
- "\n" + newText + "\n"
-
-class SplitArgumentsWithRemoveSeparator extends SplitArguments
- @extend()
- @registerToSelectList()
- keepSeparator: false
-
-class SplitArgumentsOfInnerAnyPair extends SplitArguments
- @extend()
- @registerToSelectList()
- target: "InnerAnyPair"
-
-class ChangeOrder extends TransformString
- @extend(false)
- getNewText: (text) ->
- if @target.isLinewise()
- @getNewList(splitTextByNewLine(text)).join("\n") + "\n"
- else
- @sortArgumentsInTextBy(text, (args) => @getNewList(args))
-
- sortArgumentsInTextBy: (text, fn) ->
- leadingSpaces = trailingSpaces = ''
- start = text.search(/\S/)
- end = text.search(/\s*$/)
- leadingSpaces = trailingSpaces = ''
- leadingSpaces = text[0...start] if start isnt -1
- trailingSpaces = text[end...] if end isnt -1
- text = text[start...end]
-
- allTokens = splitArguments(text)
- args = allTokens
- .filter (token) -> token.type is 'argument'
- .map (token) -> token.text
- newArgs = fn(args)
-
- newText = ''
- while allTokens.length
- {text, type} = allTokens.shift()
- newText += switch type
- when 'separator' then text
- when 'argument' then newArgs.shift()
- leadingSpaces + newText + trailingSpaces
-
-class Reverse extends ChangeOrder
- @extend()
- @registerToSelectList()
- getNewList: (rows) ->
- rows.reverse()
-
-class ReverseInnerAnyPair extends Reverse
- @extend()
- target: "InnerAnyPair"
-
-class Rotate extends ChangeOrder
- @extend()
- @registerToSelectList()
- backwards: false
- getNewList: (rows) ->
- if @backwards
- rows.push(rows.shift())
- else
- rows.unshift(rows.pop())
- rows
-
-class RotateBackwards extends ChangeOrder
- @extend()
- @registerToSelectList()
- backwards: true
-
-class RotateArgumentsOfInnerPair extends Rotate
- @extend()
- target: "InnerAnyPair"
-
-class RotateArgumentsBackwardsOfInnerPair extends RotateArgumentsOfInnerPair
- @extend()
- backwards: true
-
-class Sort extends ChangeOrder
- @extend()
- @registerToSelectList()
- @description: "Sort alphabetically"
- getNewList: (rows) ->
- rows.sort()
-
-class SortCaseInsensitively extends ChangeOrder
- @extend()
- @registerToSelectList()
- @description: "Sort alphabetically with case insensitively"
- getNewList: (rows) ->
- rows.sort (rowA, rowB) ->
- rowA.localeCompare(rowB, sensitivity: 'base')
-
-class SortByNumber extends ChangeOrder
- @extend()
- @registerToSelectList()
- @description: "Sort numerically"
- getNewList: (rows) ->
- _.sortBy rows, (row) ->
- Number.parseInt(row) or Infinity
diff --git a/lib/operator-transform-string.js b/lib/operator-transform-string.js
new file mode 100644
index 000000000..fb7cf42a3
--- /dev/null
+++ b/lib/operator-transform-string.js
@@ -0,0 +1,891 @@
+"use babel"
+
+const _ = require("underscore-plus")
+const {BufferedProcess, Range} = require("atom")
+
+const {
+ isSingleLineText,
+ isLinewiseRange,
+ limitNumber,
+ toggleCaseForCharacter,
+ splitTextByNewLine,
+ splitArguments,
+ adjustIndentWithKeepingLayout,
+} = require("./utils")
+const Base = require("./base")
+const Operator = Base.getClass("Operator")
+
+// TransformString
+// ================================
+class TransformString extends Operator {
+ static stringTransformers = []
+ trackChange = true
+ stayOptionName = "stayOnTransformString"
+ autoIndent = false
+ autoIndentNewline = false
+ autoIndentAfterInsertText = false
+
+ static registerToSelectList() {
+ this.stringTransformers.push(this)
+ }
+
+ mutateSelection(selection) {
+ const text = this.getNewText(selection.getText(), selection)
+ if (text) {
+ let startRowIndentLevel
+ if (this.autoIndentAfterInsertText) {
+ const startRow = selection.getBufferRange().start.row
+ startRowIndentLevel = this.editor.indentationForBufferRow(startRow)
+ }
+ let range = selection.insertText(text, {autoIndent: this.autoIndent, autoIndentNewline: this.autoIndentNewline})
+
+ if (this.autoIndentAfterInsertText) {
+ // Currently used by SplitArguments and Surround( linewise target only )
+ if (this.target.isLinewise()) {
+ range = range.translate([0, 0], [-1, 0])
+ }
+ this.editor.setIndentationForBufferRow(range.start.row, startRowIndentLevel)
+ this.editor.setIndentationForBufferRow(range.end.row, startRowIndentLevel)
+ // Adjust inner range, end.row is already( if needed ) translated so no need to re-translate.
+ adjustIndentWithKeepingLayout(this.editor, range.translate([1, 0], [0, 0]))
+ }
+ }
+ }
+}
+TransformString.register(false)
+
+class ToggleCase extends TransformString {
+ static displayName = "Toggle ~"
+
+ getNewText(text) {
+ return text.replace(/./g, toggleCaseForCharacter)
+ }
+}
+ToggleCase.register()
+
+class ToggleCaseAndMoveRight extends ToggleCase {
+ flashTarget = false
+ restorePositions = false
+ target = "MoveRight"
+}
+ToggleCaseAndMoveRight.register()
+
+class UpperCase extends TransformString {
+ static displayName = "Upper"
+
+ getNewText(text) {
+ return text.toUpperCase()
+ }
+}
+UpperCase.register()
+
+class LowerCase extends TransformString {
+ static displayName = "Lower"
+
+ getNewText(text) {
+ return text.toLowerCase()
+ }
+}
+LowerCase.register()
+
+// Replace
+// -------------------------
+class Replace extends TransformString {
+ flashCheckpoint = "did-select-occurrence"
+ input = null
+ requireInput = true
+ autoIndentNewline = true
+ supportEarlySelect = true
+
+ initialize() {
+ this.onDidSelectTarget(() => this.focusInput({hideCursor: true}))
+ return super.initialize()
+ }
+
+ getNewText(text) {
+ if (this.target.is("MoveRightBufferColumn") && text.length !== this.getCount()) {
+ return
+ }
+
+ const input = this.input || "\n"
+ if (input === "\n") {
+ this.restorePositions = false
+ }
+ return text.replace(/./g, input)
+ }
+}
+Replace.register()
+
+class ReplaceCharacter extends Replace {
+ target = "MoveRightBufferColumn"
+}
+ReplaceCharacter.register()
+
+// -------------------------
+// DUP meaning with SplitString need consolidate.
+class SplitByCharacter extends TransformString {
+ getNewText(text) {
+ return text.split("").join(" ")
+ }
+}
+SplitByCharacter.register()
+
+class CamelCase extends TransformString {
+ static displayName = "Camelize"
+ getNewText(text) {
+ return _.camelize(text)
+ }
+}
+CamelCase.register()
+
+class SnakeCase extends TransformString {
+ static displayName = "Underscore _"
+ getNewText(text) {
+ return _.underscore(text)
+ }
+}
+SnakeCase.register()
+
+class PascalCase extends TransformString {
+ static displayName = "Pascalize"
+ getNewText(text) {
+ return _.capitalize(_.camelize(text))
+ }
+}
+PascalCase.register()
+
+class DashCase extends TransformString {
+ static displayName = "Dasherize -"
+ getNewText(text) {
+ return _.dasherize(text)
+ }
+}
+DashCase.register()
+
+class TitleCase extends TransformString {
+ static displayName = "Titlize"
+ getNewText(text) {
+ return _.humanizeEventName(_.dasherize(text))
+ }
+}
+TitleCase.register()
+
+class EncodeUriComponent extends TransformString {
+ static displayName = "Encode URI Component %"
+ getNewText(text) {
+ return encodeURIComponent(text)
+ }
+}
+EncodeUriComponent.register()
+
+class DecodeUriComponent extends TransformString {
+ static displayName = "Decode URI Component %%"
+ getNewText(text) {
+ return decodeURIComponent(text)
+ }
+}
+DecodeUriComponent.register()
+
+class TrimString extends TransformString {
+ static displayName = "Trim string"
+ getNewText(text) {
+ return text.trim()
+ }
+}
+TrimString.register()
+
+class CompactSpaces extends TransformString {
+ static displayName = "Compact space"
+ getNewText(text) {
+ if (text.match(/^[ ]+$/)) {
+ return " "
+ } else {
+ // Don't compact for leading and trailing white spaces.
+ const regex = /^(\s*)(.*?)(\s*)$/gm
+ return text.replace(regex, (m, leading, middle, trailing) => {
+ return leading + middle.split(/[ \t]+/).join(" ") + trailing
+ })
+ }
+ }
+}
+CompactSpaces.register()
+
+class RemoveLeadingWhiteSpaces extends TransformString {
+ wise = "linewise"
+ getNewText(text, selection) {
+ const trimLeft = text => text.trimLeft()
+ return (
+ splitTextByNewLine(text)
+ .map(trimLeft)
+ .join("\n") + "\n"
+ )
+ }
+}
+RemoveLeadingWhiteSpaces.register()
+
+class ConvertToSoftTab extends TransformString {
+ static displayName = "Soft Tab"
+ wise = "linewise"
+
+ mutateSelection(selection) {
+ return this.scanForward(/\t/g, {scanRange: selection.getBufferRange()}, ({range, replace}) => {
+ // Replace \t to spaces which length is vary depending on tabStop and tabLenght
+ // So we directly consult it's screen representing length.
+ const length = this.editor.screenRangeForBufferRange(range).getExtent().column
+ return replace(" ".repeat(length))
+ })
+ }
+}
+ConvertToSoftTab.register()
+
+class ConvertToHardTab extends TransformString {
+ static displayName = "Hard Tab"
+
+ mutateSelection(selection) {
+ const tabLength = this.editor.getTabLength()
+ this.scanForward(/[ \t]+/g, {scanRange: selection.getBufferRange()}, ({range, replace}) => {
+ const {start, end} = this.editor.screenRangeForBufferRange(range)
+ let startColumn = start.column
+ const endColumn = end.column
+
+ // We can't naively replace spaces to tab, we have to consider valid tabStop column
+ // If nextTabStop column exceeds replacable range, we pad with spaces.
+ let newText = ""
+ while (true) {
+ const remainder = startColumn % tabLength
+ const nextTabStop = startColumn + (remainder === 0 ? tabLength : remainder)
+ if (nextTabStop > endColumn) {
+ newText += " ".repeat(endColumn - startColumn)
+ } else {
+ newText += "\t"
+ }
+ startColumn = nextTabStop
+ if (startColumn >= endColumn) {
+ break
+ }
+ }
+
+ replace(newText)
+ })
+ }
+}
+ConvertToHardTab.register()
+
+// -------------------------
+class TransformStringByExternalCommand extends TransformString {
+ autoIndent = true
+ command = "" // e.g. command: 'sort'
+ args = [] // e.g args: ['-rn']
+ stdoutBySelection = null
+
+ execute() {
+ this.normalizeSelectionsIfNecessary()
+ if (this.selectTarget()) {
+ return new Promise(resolve => this.collect(resolve)).then(() => {
+ for (const selection of this.editor.getSelections()) {
+ const text = this.getNewText(selection.getText(), selection)
+ selection.insertText(text, {autoIndent: this.autoIndent})
+ }
+ this.restoreCursorPositionsIfNecessary()
+ this.activateMode("normal")
+ })
+ }
+ }
+
+ collect(resolve) {
+ this.stdoutBySelection = new Map()
+ let processFinished = 0,
+ processRunning = 0
+ for (const selection of this.editor.getSelections()) {
+ const {command, args} = this.getCommand(selection) || {}
+ if (command == null || args == null) return
+
+ processRunning++
+ this.runExternalCommand({
+ command: command,
+ args: args,
+ stdin: this.getStdin(selection),
+ stdout: output => this.stdoutBySelection.set(selection, output),
+ exit: code => {
+ processFinished++
+ if (processRunning === processFinished) resolve()
+ },
+ })
+ }
+ }
+
+ runExternalCommand(options) {
+ const {stdin} = options
+ delete options.stdin
+ const bufferedProcess = new BufferedProcess(options)
+ bufferedProcess.onWillThrowError(({error, handle}) => {
+ // Suppress command not found error intentionally.
+ if (error.code === "ENOENT" && error.syscall.indexOf("spawn") === 0) {
+ console.log(`${this.getCommandName()}: Failed to spawn command ${error.path}.`)
+ handle()
+ }
+ this.cancelOperation()
+ })
+
+ if (stdin) {
+ bufferedProcess.process.stdin.write(stdin)
+ bufferedProcess.process.stdin.end()
+ }
+ }
+
+ getNewText(text, selection) {
+ return this.getStdout(selection) || text
+ }
+
+ // For easily extend by vmp plugin.
+ getCommand(selection) {
+ return {command: this.command, args: this.args}
+ }
+ getStdin(selection) {
+ return selection.getText()
+ }
+ getStdout(selection) {
+ return this.stdoutBySelection.get(selection)
+ }
+}
+TransformStringByExternalCommand.register(false)
+
+// -------------------------
+class TransformStringBySelectList extends TransformString {
+ static electListItems = null
+ requireInput = true
+
+ static getSelectListItems() {
+ if (!this.selectListItems) {
+ this.selectListItems = this.stringTransformers.map(klass => ({
+ klass: klass,
+ displayName: klass.hasOwnProperty("displayName")
+ ? klass.displayName
+ : _.humanizeEventName(_.dasherize(klass.name)),
+ }))
+ }
+ return this.selectListItems
+ }
+
+ getItems() {
+ return this.constructor.getSelectListItems()
+ }
+
+ initialize() {
+ this.vimState.onDidConfirmSelectList(item => {
+ const transformer = item.klass
+ if (transformer.prototype.target) {
+ this.target = transformer.prototype.target
+ }
+ this.vimState.reset()
+ if (this.target) {
+ this.vimState.operationStack.run(transformer, {target: this.target})
+ } else {
+ this.vimState.operationStack.run(transformer)
+ }
+ })
+
+ this.focusSelectList({items: this.getItems()})
+
+ return super.initialize()
+ }
+
+ execute() {
+ // NEVER be executed since operationStack is replaced with selected transformer
+ throw new Error(`${this.name} should not be executed`)
+ }
+}
+TransformStringBySelectList.register()
+
+class TransformWordBySelectList extends TransformStringBySelectList {
+ target = "InnerWord"
+}
+TransformWordBySelectList.register()
+
+class TransformSmartWordBySelectList extends TransformStringBySelectList {
+ target = "InnerSmartWord"
+}
+TransformSmartWordBySelectList.register()
+
+// -------------------------
+class ReplaceWithRegister extends TransformString {
+ flashType = "operator-long"
+
+ initialize() {
+ this.vimState.sequentialPasteManager.onInitialize(this)
+ return super.initialize()
+ }
+
+ execute() {
+ this.sequentialPaste = this.vimState.sequentialPasteManager.onExecute(this)
+
+ super.execute()
+
+ for (const selection of this.editor.getSelections()) {
+ const range = this.mutationManager.getMutatedBufferRangeForSelection(selection)
+ this.vimState.sequentialPasteManager.savePastedRangeForSelection(selection, range)
+ }
+ }
+
+ getNewText(text, selection) {
+ const value = this.vimState.register.get(null, selection, this.sequentialPaste)
+ return value ? value.text : ""
+ }
+}
+ReplaceWithRegister.register()
+
+// Save text to register before replace
+class SwapWithRegister extends TransformString {
+ getNewText(text, selection) {
+ const newText = this.vimState.register.getText()
+ this.setTextToRegister(text, selection)
+ return newText
+ }
+}
+SwapWithRegister.register()
+
+// Indent < TransformString
+// -------------------------
+class Indent extends TransformString {
+ stayByMarker = true
+ setToFirstCharacterOnLinewise = true
+ wise = "linewise"
+
+ mutateSelection(selection) {
+ // Need count times indentation in visual-mode and its repeat(`.`).
+ if (this.target.is("CurrentSelection")) {
+ let oldText
+ // limit to 100 to avoid freezing by accidental big number.
+ const count = limitNumber(this.getCount(), {max: 100})
+ this.countTimes(count, ({stop}) => {
+ oldText = selection.getText()
+ this.indent(selection)
+ if (selection.getText() === oldText) stop()
+ })
+ } else {
+ this.indent(selection)
+ }
+ }
+
+ indent(selection) {
+ selection.indentSelectedRows()
+ }
+}
+Indent.register()
+
+class Outdent extends Indent {
+ indent(selection) {
+ selection.outdentSelectedRows()
+ }
+}
+Outdent.register()
+
+class AutoIndent extends Indent {
+ indent(selection) {
+ selection.autoIndentSelectedRows()
+ }
+}
+AutoIndent.register()
+
+class ToggleLineComments extends TransformString {
+ flashTarget = false
+ stayByMarker = true
+ wise = "linewise"
+
+ mutateSelection(selection) {
+ selection.toggleLineComments()
+ }
+}
+ToggleLineComments.register()
+
+class Reflow extends TransformString {
+ mutateSelection(selection) {
+ atom.commands.dispatch(this.editorElement, "autoflow:reflow-selection")
+ }
+}
+Reflow.register()
+
+class ReflowWithStay extends Reflow {
+ stayAtSamePosition = true
+}
+ReflowWithStay.register()
+
+// Surround < TransformString
+// -------------------------
+class SurroundBase extends TransformString {
+ pairs = [["(", ")"], ["{", "}"], ["[", "]"], ["<", ">"]]
+ pairsByAlias = {
+ b: ["(", ")"],
+ B: ["{", "}"],
+ r: ["[", "]"],
+ a: ["<", ">"],
+ }
+
+ pairCharsAllowForwarding = "[](){}"
+ input = null
+ requireInput = true
+ supportEarlySelect = true // Experimental
+
+ focusInputForSurroundChar() {
+ this.focusInput({hideCursor: true})
+ }
+
+ focusInputForTargetPairChar() {
+ this.focusInput({onConfirm: char => this.onConfirmTargetPairChar(char)})
+ }
+
+ getPair(char) {
+ let pair
+ return char in this.pairsByAlias
+ ? this.pairsByAlias[char]
+ : [...this.pairs, [char, char]].find(pair => pair.includes(char))
+ }
+
+ surround(text, char, {keepLayout = false} = {}) {
+ let [open, close] = this.getPair(char)
+ if (!keepLayout && text.endsWith("\n")) {
+ this.autoIndentAfterInsertText = true
+ open += "\n"
+ close += "\n"
+ }
+
+ if (this.getConfig("charactersToAddSpaceOnSurround").includes(char) && isSingleLineText(text)) {
+ text = " " + text + " "
+ }
+
+ return open + text + close
+ }
+
+ deleteSurround(text) {
+ // Assume surrounding char is one-char length.
+ const open = text[0]
+ const close = text[text.length - 1]
+ const innerText = text.slice(1, text.length - 1)
+ return isSingleLineText(text) && open !== close ? innerText.trim() : innerText
+ }
+
+ onConfirmTargetPairChar(char) {
+ this.setTarget(this.getInstance("APair", {pair: this.getPair(char)}))
+ }
+}
+SurroundBase.register(false)
+
+class Surround extends SurroundBase {
+ initialize() {
+ this.onDidSelectTarget(() => this.focusInputForSurroundChar())
+ return super.initialize()
+ }
+
+ getNewText(text) {
+ return this.surround(text, this.input)
+ }
+}
+Surround.register()
+
+class SurroundWord extends Surround {
+ target = "InnerWord"
+}
+SurroundWord.register()
+
+class SurroundSmartWord extends Surround {
+ target = "InnerSmartWord"
+}
+SurroundSmartWord.register()
+
+class MapSurround extends Surround {
+ occurrence = true
+ patternForOccurrence = /\w+/g
+}
+MapSurround.register()
+
+// Delete Surround
+// -------------------------
+class DeleteSurround extends SurroundBase {
+ initialize() {
+ if (!this.target) {
+ this.focusInputForTargetPairChar()
+ }
+ return super.initialize()
+ }
+
+ onConfirmTargetPairChar(char) {
+ super.onConfirmTargetPairChar(char)
+ this.input = char
+ this.processOperation()
+ }
+
+ getNewText(text) {
+ return this.deleteSurround(text)
+ }
+}
+DeleteSurround.register()
+
+class DeleteSurroundAnyPair extends DeleteSurround {
+ target = "AAnyPair"
+ requireInput = false
+}
+DeleteSurroundAnyPair.register()
+
+class DeleteSurroundAnyPairAllowForwarding extends DeleteSurroundAnyPair {
+ target = "AAnyPairAllowForwarding"
+}
+DeleteSurroundAnyPairAllowForwarding.register()
+
+// Change Surround
+// -------------------------
+class ChangeSurround extends SurroundBase {
+ showDeleteCharOnHover() {
+ const hoverPoint = this.mutationManager.getInitialPointForSelection(this.editor.getLastSelection())
+ const char = this.editor.getSelectedText()[0]
+ this.vimState.hover.set(char, hoverPoint)
+ }
+
+ initialize() {
+ if (this.target) {
+ this.onDidFailSelectTarget(() => this.abort())
+ } else {
+ this.onDidFailSelectTarget(() => this.cancelOperation())
+ this.focusInputForTargetPairChar()
+ }
+
+ this.onDidSelectTarget(() => {
+ this.showDeleteCharOnHover()
+ this.focusInputForSurroundChar()
+ })
+ return super.initialize()
+ }
+
+ getNewText(text) {
+ const innerText = this.deleteSurround(text)
+ return this.surround(innerText, this.input, {keepLayout: true})
+ }
+}
+ChangeSurround.register()
+
+class ChangeSurroundAnyPair extends ChangeSurround {
+ target = "AAnyPair"
+}
+ChangeSurroundAnyPair.register()
+
+class ChangeSurroundAnyPairAllowForwarding extends ChangeSurroundAnyPair {
+ target = "AAnyPairAllowForwarding"
+}
+ChangeSurroundAnyPairAllowForwarding.register()
+
+// -------------------------
+// FIXME
+// Currently native editor.joinLines() is better for cursor position setting
+// So I use native methods for a meanwhile.
+class Join extends TransformString {
+ target = "MoveToRelativeLine"
+ flashTarget = false
+ restorePositions = false
+
+ mutateSelection(selection) {
+ const range = selection.getBufferRange()
+
+ // When cursor is at last BUFFER row, it select last-buffer-row, then
+ // joinning result in "clear last-buffer-row text".
+ // I believe this is BUG of upstream atom-core. guard this situation here
+ if (!range.isSingleLine() || range.end.row !== this.editor.getLastBufferRow()) {
+ if (isLinewiseRange(range)) {
+ selection.setBufferRange(range.translate([0, 0], [-1, Infinity]))
+ }
+ selection.joinLines()
+ }
+ const point = selection.getBufferRange().end.translate([0, -1])
+ return selection.cursor.setBufferPosition(point)
+ }
+}
+Join.register()
+
+class JoinBase extends TransformString {
+ wise = "linewise"
+ trim = false
+ target = "MoveToRelativeLineMinimumTwo"
+
+ initialize() {
+ if (this.requireInput) {
+ this.focusInput({charsMax: 10})
+ }
+ return super.initialize()
+ }
+
+ getNewText(text) {
+ const regex = this.trim ? /\r?\n[ \t]*/g : /\r?\n/g
+ return text.trimRight().replace(regex, this.input) + "\n"
+ }
+}
+JoinBase.register(false)
+
+class JoinWithKeepingSpace extends JoinBase {
+ input = ""
+}
+JoinWithKeepingSpace.register()
+
+class JoinByInput extends JoinBase {
+ requireInput = true
+ trim = true
+}
+JoinByInput.register()
+
+class JoinByInputWithKeepingSpace extends JoinByInput {
+ trim = false
+}
+JoinByInputWithKeepingSpace.register()
+
+// -------------------------
+// String suffix in name is to avoid confusion with 'split' window.
+class SplitString extends TransformString {
+ requireInput = true
+ input = null
+ target = "MoveToRelativeLine"
+ keepSplitter = false
+
+ initialize() {
+ this.onDidSetTarget(() => {
+ this.focusInput({charsMax: 10})
+ })
+ return super.initialize()
+ }
+
+ getNewText(text) {
+ const regex = new RegExp(_.escapeRegExp(this.input || "\\n"), "g")
+ const lineSeparator = (this.keepSplitter ? this.input : "") + "\n"
+ return text.replace(regex, lineSeparator)
+ }
+}
+SplitString.register()
+
+class SplitStringWithKeepingSplitter extends SplitString {
+ keepSplitter = true
+}
+SplitStringWithKeepingSplitter.register()
+
+class SplitArguments extends TransformString {
+ keepSeparator = true
+ autoIndentAfterInsertText = true
+
+ getNewText(text) {
+ const allTokens = splitArguments(text.trim())
+ let newText = ""
+ while (allTokens.length) {
+ const {text, type} = allTokens.shift()
+ newText += type === "separator" ? (this.keepSeparator ? text.trim() : "") + "\n" : text
+ }
+ return `\n${newText}\n`
+ }
+}
+SplitArguments.register()
+
+class SplitArgumentsWithRemoveSeparator extends SplitArguments {
+ keepSeparator = false
+}
+SplitArgumentsWithRemoveSeparator.register()
+
+class SplitArgumentsOfInnerAnyPair extends SplitArguments {
+ target = "InnerAnyPair"
+}
+SplitArgumentsOfInnerAnyPair.register()
+
+class ChangeOrder extends TransformString {
+ getNewText(text) {
+ return this.target.isLinewise()
+ ? this.getNewList(splitTextByNewLine(text)).join("\n") + "\n"
+ : this.sortArgumentsInTextBy(text, args => this.getNewList(args))
+ }
+
+ sortArgumentsInTextBy(text, fn) {
+ const start = text.search(/\S/)
+ const end = text.search(/\s*$/)
+ const leadingSpaces = start !== -1 ? text.slice(0, start) : ""
+ const trailingSpaces = end !== -1 ? text.slice(end) : ""
+ const allTokens = splitArguments(text.slice(start, end))
+ const args = allTokens.filter(token => token.type === "argument").map(token => token.text)
+ const newArgs = fn(args)
+
+ let newText = ""
+ while (allTokens.length) {
+ const token = allTokens.shift()
+ // token.type is "separator" or "argument"
+ newText += token.type === "separator" ? token.text : newArgs.shift()
+ }
+ return leadingSpaces + newText + trailingSpaces
+ }
+}
+ChangeOrder.register(false)
+
+class Reverse extends ChangeOrder {
+ getNewList(rows) {
+ return rows.reverse()
+ }
+}
+Reverse.register()
+
+class ReverseInnerAnyPair extends Reverse {
+ target = "InnerAnyPair"
+}
+ReverseInnerAnyPair.register()
+
+class Rotate extends ChangeOrder {
+ backwards = false
+ getNewList(rows) {
+ if (this.backwards) rows.push(rows.shift())
+ else rows.unshift(rows.pop())
+ return rows
+ }
+}
+Rotate.register()
+
+class RotateBackwards extends ChangeOrder {
+ backwards = true
+}
+RotateBackwards.register()
+
+class RotateArgumentsOfInnerPair extends Rotate {
+ target = "InnerAnyPair"
+}
+RotateArgumentsOfInnerPair.register()
+
+class RotateArgumentsBackwardsOfInnerPair extends RotateArgumentsOfInnerPair {
+ backwards = true
+}
+RotateArgumentsBackwardsOfInnerPair.register()
+
+class Sort extends ChangeOrder {
+ getNewList(rows) {
+ return rows.sort()
+ }
+}
+Sort.register()
+
+class SortCaseInsensitively extends ChangeOrder {
+ getNewList(rows) {
+ return rows.sort((rowA, rowB) => rowA.localeCompare(rowB, {sensitivity: "base"}))
+ }
+}
+SortCaseInsensitively.register()
+
+class SortByNumber extends ChangeOrder {
+ getNewList(rows) {
+ return _.sortBy(rows, row => Number.parseInt(row) || Infinity)
+ }
+}
+SortByNumber.register()
+
+// prettier-ignore
+const classesToRegisterToSelectList = [
+ ToggleCase, UpperCase, LowerCase,
+ Replace, SplitByCharacter,
+ CamelCase, SnakeCase, PascalCase, DashCase, TitleCase,
+ EncodeUriComponent, DecodeUriComponent,
+ TrimString, CompactSpaces, RemoveLeadingWhiteSpaces,
+ ConvertToSoftTab, ConvertToHardTab,
+ JoinWithKeepingSpace, JoinByInput, JoinByInputWithKeepingSpace,
+ SplitString, SplitStringWithKeepingSplitter,
+ SplitArguments, SplitArgumentsWithRemoveSeparator, SplitArgumentsOfInnerAnyPair,
+ Reverse, Rotate, RotateBackwards, Sort, SortCaseInsensitively, SortByNumber,
+]
+for (const klass of classesToRegisterToSelectList) {
+ klass.registerToSelectList()
+}
diff --git a/lib/operator.coffee b/lib/operator.coffee
deleted file mode 100644
index ec6a36c08..000000000
--- a/lib/operator.coffee
+++ /dev/null
@@ -1,692 +0,0 @@
-_ = require 'underscore-plus'
-{
- isEmptyRow
- getWordPatternAtBufferPosition
- getSubwordPatternAtBufferPosition
- insertTextAtBufferPosition
- setBufferRow
- moveCursorToFirstCharacterAtRow
- ensureEndsWithNewLineForBufferRow
- adjustIndentWithKeepingLayout
- isSingleLineText
-} = require './utils'
-Base = require './base'
-
-class Operator extends Base
- @extend(false)
- @operationKind: 'operator'
- requireTarget: true
- recordable: true
-
- wise: null
- occurrence: false
- occurrenceType: 'base'
-
- flashTarget: true
- flashCheckpoint: 'did-finish'
- flashType: 'operator'
- flashTypeForOccurrence: 'operator-occurrence'
- trackChange: false
-
- patternForOccurrence: null
- stayAtSamePosition: null
- stayOptionName: null
- stayByMarker: false
- restorePositions: true
- setToFirstCharacterOnLinewise: false
-
- acceptPresetOccurrence: true
- acceptPersistentSelection: true
-
- bufferCheckpointByPurpose: null
- mutateSelectionOrderd: false
-
- # Experimentaly allow selectTarget before input Complete
- # -------------------------
- supportEarlySelect: false
- targetSelected: null
- canEarlySelect: ->
- @supportEarlySelect and not @repeated
- # -------------------------
-
- # Called when operation finished
- # This is essentially to reset state for `.` repeat.
- resetState: ->
- @targetSelected = null
- @occurrenceSelected = false
-
- # Two checkpoint for different purpose
- # - one for undo(handled by modeManager)
- # - one for preserve last inserted text
- createBufferCheckpoint: (purpose) ->
- @bufferCheckpointByPurpose ?= {}
- @bufferCheckpointByPurpose[purpose] = @editor.createCheckpoint()
-
- getBufferCheckpoint: (purpose) ->
- @bufferCheckpointByPurpose?[purpose]
-
- deleteBufferCheckpoint: (purpose) ->
- if @bufferCheckpointByPurpose?
- delete @bufferCheckpointByPurpose[purpose]
-
- groupChangesSinceBufferCheckpoint: (purpose) ->
- if checkpoint = @getBufferCheckpoint(purpose)
- @editor.groupChangesSinceCheckpoint(checkpoint)
- @deleteBufferCheckpoint(purpose)
-
- setMarkForChange: (range) ->
- @vimState.mark.set('[', range.start)
- @vimState.mark.set(']', range.end)
-
- needFlash: ->
- @flashTarget and @getConfig('flashOnOperate') and
- (@name not in @getConfig('flashOnOperateBlacklist')) and
- ((@mode isnt 'visual') or (@submode isnt @target.wise)) # e.g. Y in vC
-
- flashIfNecessary: (ranges) ->
- if @needFlash()
- @vimState.flash(ranges, type: @getFlashType())
-
- flashChangeIfNecessary: ->
- if @needFlash()
- @onDidFinishOperation =>
- ranges = @mutationManager.getSelectedBufferRangesForCheckpoint(@flashCheckpoint)
- @vimState.flash(ranges, type: @getFlashType())
-
- getFlashType: ->
- if @occurrenceSelected
- @flashTypeForOccurrence
- else
- @flashType
-
- trackChangeIfNecessary: ->
- return unless @trackChange
-
- @onDidFinishOperation =>
- if range = @mutationManager.getMutatedBufferRangeForSelection(@editor.getLastSelection())
- @setMarkForChange(range)
-
- constructor: ->
- super
- {@mutationManager, @occurrenceManager, @persistentSelection} = @vimState
- @subscribeResetOccurrencePatternIfNeeded()
- @initialize()
- @onDidSetOperatorModifier(@setModifier.bind(this))
-
- # When preset-occurrence was exists, operate on occurrence-wise
- if @acceptPresetOccurrence and @occurrenceManager.hasMarkers()
- @occurrence = true
-
- # [FIXME] ORDER-MATTER
- # To pick cursor-word to find occurrence base pattern.
- # This has to be done BEFORE converting persistent-selection into real-selection.
- # Since when persistent-selection is actuall selected, it change cursor position.
- if @occurrence and not @occurrenceManager.hasMarkers()
- @occurrenceManager.addPattern(@patternForOccurrence ? @getPatternForOccurrenceType(@occurrenceType))
-
-
- # This change cursor position.
- if @selectPersistentSelectionIfNecessary()
- # [FIXME] selection-wise is not synched if it already visual-mode
- unless @mode is 'visual'
- @vimState.modeManager.activate('visual', @swrap.detectWise(@editor))
-
- @target = 'CurrentSelection' if @mode is 'visual' and @requireTarget
- @setTarget(@new(@target)) if _.isString(@target)
-
- subscribeResetOccurrencePatternIfNeeded: ->
- # [CAUTION]
- # This method has to be called in PROPER timing.
- # If occurrence is true but no preset-occurrence
- # Treat that `occurrence` is BOUNDED to operator itself, so cleanp at finished.
- if @occurrence and not @occurrenceManager.hasMarkers()
- @onDidResetOperationStack(=> @occurrenceManager.resetPatterns())
-
- setModifier: (options) ->
- if options.wise?
- @wise = options.wise
- return
-
- if options.occurrence?
- @occurrence = options.occurrence
- if @occurrence
- @occurrenceType = options.occurrenceType
- # This is o modifier case(e.g. `c o p`, `d O f`)
- # We RESET existing occurence-marker when `o` or `O` modifier is typed by user.
- pattern = @getPatternForOccurrenceType(@occurrenceType)
- @occurrenceManager.addPattern(pattern, {reset: true, @occurrenceType})
- @onDidResetOperationStack(=> @occurrenceManager.resetPatterns())
-
- # return true/false to indicate success
- selectPersistentSelectionIfNecessary: ->
- if @acceptPersistentSelection and
- @getConfig('autoSelectPersistentSelectionOnOperate') and
- not @persistentSelection.isEmpty()
-
- @persistentSelection.select()
- @editor.mergeIntersectingSelections()
- for $selection in @swrap.getSelections(@editor) when not $selection.hasProperties()
- $selection.saveProperties()
- true
- else
- false
-
- getPatternForOccurrenceType: (occurrenceType) ->
- switch occurrenceType
- when 'base'
- getWordPatternAtBufferPosition(@editor, @getCursorBufferPosition())
- when 'subword'
- getSubwordPatternAtBufferPosition(@editor, @getCursorBufferPosition())
-
- # target is TextObject or Motion to operate on.
- setTarget: (@target) ->
- @target.operator = this
- @emitDidSetTarget(this)
-
- if @canEarlySelect()
- @normalizeSelectionsIfNecessary()
- @createBufferCheckpoint('undo')
- @selectTarget()
- this
-
- setTextToRegisterForSelection: (selection) ->
- @setTextToRegister(selection.getText(), selection)
-
- setTextToRegister: (text, selection) ->
- text += "\n" if (@target.isLinewise() and (not text.endsWith('\n')))
- if text
- @vimState.register.set(null, {text, selection})
-
- if @vimState.register.isUnnamed()
- if @instanceof("Delete") or @instanceof("Change")
- if not @needSaveToNumberedRegister(@target) and isSingleLineText(text) # small-change
- @vimState.register.set('-', {text, selection})
- else
- @vimState.register.set('1', {text, selection})
-
- else if @instanceof("Yank")
- @vimState.register.set('0', {text, selection})
-
- needSaveToNumberedRegister: (target) ->
- # Used to determine what register to use on change and delete operation.
- # Following motion should save to 1-9 register regerdless of content is small or big.
- goesToNumberedRegisterMotionNames = [
- "MoveToPair" # %
- "MoveToNextSentence" # (, )
- "Search" # /, ?, n, N
- "MoveToNextParagraph" # {, }
- ]
- goesToNumberedRegisterMotionNames.some((name) -> target.instanceof(name))
-
- normalizeSelectionsIfNecessary: ->
- if @target?.isMotion() and (@mode is 'visual')
- @swrap.normalize(@editor)
-
- startMutation: (fn) ->
- if @canEarlySelect()
- # - Skip selection normalization: already normalized before @selectTarget()
- # - Manual checkpoint grouping: to create checkpoint before @selectTarget()
- fn()
- @emitWillFinishMutation()
- @groupChangesSinceBufferCheckpoint('undo')
-
- else
- @normalizeSelectionsIfNecessary()
- @editor.transact =>
- fn()
- @emitWillFinishMutation()
-
- @emitDidFinishMutation()
-
- # Main
- execute: ->
- @startMutation =>
- if @selectTarget()
- if @mutateSelectionOrderd
- selections = @editor.getSelectionsOrderedByBufferPosition()
- else
- selections = @editor.getSelections()
- for selection in selections
- @mutateSelection(selection)
- @mutationManager.setCheckpoint('did-finish')
- @restoreCursorPositionsIfNecessary()
-
- # Even though we fail to select target and fail to mutate,
- # we have to return to normal-mode from operator-pending or visual
- @activateMode('normal')
-
- # Return true unless all selection is empty.
- selectTarget: ->
- return @targetSelected if @targetSelected?
- @mutationManager.init({@stayByMarker})
-
- @target.forceWise(@wise) if @wise?
- @emitWillSelectTarget()
-
- # Allow cursor position adjustment 'on-will-select-target' hook.
- # so checkpoint comes AFTER @emitWillSelectTarget()
- @mutationManager.setCheckpoint('will-select')
-
- # NOTE
- # Since MoveToNextOccurrence, MoveToPreviousOccurrence motion move by
- # occurrence-marker, occurrence-marker has to be created BEFORE `@target.execute()`
- # And when repeated, occurrence pattern is already cached at @patternForOccurrence
- if @repeated and @occurrence and not @occurrenceManager.hasMarkers()
- @occurrenceManager.addPattern(@patternForOccurrence, {@occurrenceType})
-
- @target.execute()
-
- @mutationManager.setCheckpoint('did-select')
- if @occurrence
- # To repoeat(`.`) operation where multiple occurrence patterns was set.
- # Here we save patterns which represent unioned regex which @occurrenceManager knows.
- @patternForOccurrence ?= @occurrenceManager.buildPattern()
-
- @occurrenceWise = @wise ? "characterwise"
- if @occurrenceManager.select(@occurrenceWise)
- @occurrenceSelected = true
- @mutationManager.setCheckpoint('did-select-occurrence')
-
- if @targetSelected = @vimState.haveSomeNonEmptySelection() or @target.name is "Empty"
- @emitDidSelectTarget()
- @flashChangeIfNecessary()
- @trackChangeIfNecessary()
- else
- @emitDidFailSelectTarget()
- return @targetSelected
-
- restoreCursorPositionsIfNecessary: ->
- return unless @restorePositions
- stay = @stayAtSamePosition ? @getConfig(@stayOptionName) or (@occurrenceSelected and @getConfig('stayOnOccurrence'))
- wise = if @occurrenceSelected then @occurrenceWise else @target.wise
- @mutationManager.restoreCursorPositions({stay, wise, @setToFirstCharacterOnLinewise})
-
-class SelectBase extends Operator
- @extend(false)
- flashTarget: false
- recordable: false
-
- execute: ->
- @startMutation => @selectTarget()
-
- if (@target.selectSucceeded)
- @editor.scrollToCursorPosition() if @target.isTextObject()
- wise = if @occurrenceSelected then @occurrenceWise else @target.wise
- @activateModeIfNecessary('visual', wise)
- else
- @cancelOperation()
-
-class Select extends SelectBase
- @extend()
- execute: ->
- for $selection in @swrap.getSelections(@editor) when not $selection.hasProperties()
- $selection.saveProperties()
- super
-
-class SelectLatestChange extends SelectBase
- @extend()
- @description: "Select latest yanked or changed range"
- target: 'ALatestChange'
-
-class SelectPreviousSelection extends SelectBase
- @extend()
- target: "PreviousSelection"
-
-class SelectPersistentSelection extends SelectBase
- @extend()
- @description: "Select persistent-selection and clear all persistent-selection, it's like convert to real-selection"
- target: "APersistentSelection"
- acceptPersistentSelection: false
-
-class SelectOccurrence extends SelectBase
- @extend()
- @description: "Add selection onto each matching word within target range"
- occurrence: true
-
-# SelectInVisualMode: used in visual-mode
-# When text-object is invoked from normal or viusal-mode, operation would be
-# => SelectInVisualMode operator with target=text-object
-# When motion is invoked from visual-mode, operation would be
-# => SelectInVisualMode operator with target=motion)
-# ================================
-# SelectInVisualMode is used in TWO situation.
-# - visual-mode operation
-# - e.g: `v l`, `V j`, `v i p`...
-# - Directly invoke text-object from normal-mode
-# - e.g: Invoke `Inner Paragraph` from command-palette.
-class SelectInVisualMode extends SelectBase
- @extend(false)
- acceptPresetOccurrence: false
- acceptPersistentSelection: false
-
-# Persistent Selection
-# =========================
-class CreatePersistentSelection extends Operator
- @extend()
- flashTarget: false
- stayAtSamePosition: true
- acceptPresetOccurrence: false
- acceptPersistentSelection: false
-
- mutateSelection: (selection) ->
- @persistentSelection.markBufferRange(selection.getBufferRange())
-
-class TogglePersistentSelection extends CreatePersistentSelection
- @extend()
-
- isComplete: ->
- point = @editor.getCursorBufferPosition()
- @markerToRemove = @persistentSelection.getMarkerAtPoint(point)
- @markerToRemove? or super
-
- execute: ->
- if @markerToRemove?
- @markerToRemove.destroy()
- else
- super
-
-# Preset Occurrence
-# =========================
-class TogglePresetOccurrence extends Operator
- @extend()
- target: "Empty"
- flashTarget: false
- acceptPresetOccurrence: false
- acceptPersistentSelection: false
- occurrenceType: 'base'
-
- execute: ->
- if marker = @occurrenceManager.getMarkerAtPoint(@editor.getCursorBufferPosition())
- @occurrenceManager.destroyMarkers([marker])
- else
- pattern = null
- isNarrowed = @vimState.modeManager.isNarrowed()
-
- if @mode is 'visual' and not isNarrowed
- @occurrenceType = 'base'
- pattern = new RegExp(_.escapeRegExp(@editor.getSelectedText()), 'g')
- else
- pattern = @getPatternForOccurrenceType(@occurrenceType)
-
- @occurrenceManager.addPattern(pattern, {@occurrenceType})
- @occurrenceManager.saveLastPattern(@occurrenceType)
-
- @activateMode('normal') unless isNarrowed
-
-class TogglePresetSubwordOccurrence extends TogglePresetOccurrence
- @extend()
- occurrenceType: 'subword'
-
-# Want to rename RestoreOccurrenceMarker
-class AddPresetOccurrenceFromLastOccurrencePattern extends TogglePresetOccurrence
- @extend()
- execute: ->
- @occurrenceManager.resetPatterns()
- if pattern = @vimState.globalState.get('lastOccurrencePattern')
- occurrenceType = @vimState.globalState.get("lastOccurrenceType")
- @occurrenceManager.addPattern(pattern, {occurrenceType})
- @activateMode('normal')
-
-# Delete
-# ================================
-class Delete extends Operator
- @extend()
- trackChange: true
- flashCheckpoint: 'did-select-occurrence'
- flashTypeForOccurrence: 'operator-remove-occurrence'
- stayOptionName: 'stayOnDelete'
- setToFirstCharacterOnLinewise: true
-
- execute: ->
- @onDidSelectTarget =>
- @flashTarget = false if (@occurrenceSelected and @occurrenceWise is "linewise")
-
- @restorePositions = false if @target.wise is 'blockwise'
- super
-
- mutateSelection: (selection) ->
- @setTextToRegisterForSelection(selection)
- selection.deleteSelectedText()
-
-class DeleteRight extends Delete
- @extend()
- target: 'MoveRight'
-
-class DeleteLeft extends Delete
- @extend()
- target: 'MoveLeft'
-
-class DeleteToLastCharacterOfLine extends Delete
- @extend()
- target: 'MoveToLastCharacterOfLine'
-
- execute: ->
- if @target.wise is 'blockwise'
- @onDidSelectTarget =>
- for blockwiseSelection in @getBlockwiseSelections()
- blockwiseSelection.extendMemberSelectionsToEndOfLine()
- super
-
-class DeleteLine extends Delete
- @extend()
- wise: 'linewise'
- target: "MoveToRelativeLine"
- flashTarget: false
-
-# Yank
-# =========================
-class Yank extends Operator
- @extend()
- trackChange: true
- stayOptionName: 'stayOnYank'
-
- mutateSelection: (selection) ->
- @setTextToRegisterForSelection(selection)
-
-class YankLine extends Yank
- @extend()
- wise: 'linewise'
- target: "MoveToRelativeLine"
-
-class YankToLastCharacterOfLine extends Yank
- @extend()
- target: 'MoveToLastCharacterOfLine'
-
-# -------------------------
-# [ctrl-a]
-class Increase extends Operator
- @extend()
- target: "Empty" # ctrl-a in normal-mode find target number in current line manually
- flashTarget: false # do manually
- restorePositions: false # do manually
- step: 1
-
- execute: ->
- @newRanges = []
- super
- if @newRanges.length
- if @getConfig('flashOnOperate') and @name not in @getConfig('flashOnOperateBlacklist')
- @vimState.flash(@newRanges, type: @flashTypeForOccurrence)
-
- replaceNumberInBufferRange: (scanRange, fn=null) ->
- newRanges = []
- @pattern ?= ///#{@getConfig('numberRegex')}///g
- @scanForward @pattern, {scanRange}, (event) =>
- return if fn? and not fn(event)
- {matchText, replace} = event
- nextNumber = @getNextNumber(matchText)
- newRanges.push(replace(String(nextNumber)))
- newRanges
-
- mutateSelection: (selection) ->
- {cursor} = selection
- if @target.is('Empty') # ctrl-a, ctrl-x in `normal-mode`
- cursorPosition = cursor.getBufferPosition()
- scanRange = @editor.bufferRangeForBufferRow(cursorPosition.row)
- newRanges = @replaceNumberInBufferRange scanRange, ({range, stop}) ->
- if range.end.isGreaterThan(cursorPosition)
- stop()
- true
- else
- false
-
- point = newRanges[0]?.end.translate([0, -1]) ? cursorPosition
- cursor.setBufferPosition(point)
- else
- scanRange = selection.getBufferRange()
- @newRanges.push(@replaceNumberInBufferRange(scanRange)...)
- cursor.setBufferPosition(scanRange.start)
-
- getNextNumber: (numberString) ->
- Number.parseInt(numberString, 10) + @step * @getCount()
-
-# [ctrl-x]
-class Decrease extends Increase
- @extend()
- step: -1
-
-# -------------------------
-# [g ctrl-a]
-class IncrementNumber extends Increase
- @extend()
- baseNumber: null
- target: null
- mutateSelectionOrderd: true
-
- getNextNumber: (numberString) ->
- if @baseNumber?
- @baseNumber += @step * @getCount()
- else
- @baseNumber = Number.parseInt(numberString, 10)
- @baseNumber
-
-# [g ctrl-x]
-class DecrementNumber extends IncrementNumber
- @extend()
- step: -1
-
-# Put
-# -------------------------
-# Cursor placement:
-# - place at end of mutation: paste non-multiline characterwise text
-# - place at start of mutation: non-multiline characterwise text(characterwise, linewise)
-class PutBefore extends Operator
- @extend()
- location: 'before'
- target: 'Empty'
- flashType: 'operator-long'
- restorePositions: false # manage manually
- flashTarget: false # manage manually
- trackChange: false # manage manually
-
- initialize: ->
- @vimState.sequentialPasteManager.onInitialize(this)
-
- execute: ->
- @mutationsBySelection = new Map()
- @sequentialPaste = @vimState.sequentialPasteManager.onExecute(this)
-
- @onDidFinishMutation =>
- @adjustCursorPosition() unless @cancelled
-
- super
-
- return if @cancelled
-
- @onDidFinishOperation =>
- # TrackChange
- if newRange = @mutationsBySelection.get(@editor.getLastSelection())
- @setMarkForChange(newRange)
-
- # Flash
- if @getConfig('flashOnOperate') and @name not in @getConfig('flashOnOperateBlacklist')
- toRange = (selection) => @mutationsBySelection.get(selection)
- @vimState.flash(@editor.getSelections().map(toRange), type: @getFlashType())
-
- adjustCursorPosition: ->
- for selection in @editor.getSelections() when @mutationsBySelection.has(selection)
- {cursor} = selection
- {start, end} = newRange = @mutationsBySelection.get(selection)
- if @linewisePaste
- moveCursorToFirstCharacterAtRow(cursor, start.row)
- else
- if newRange.isSingleLine()
- cursor.setBufferPosition(end.translate([0, -1]))
- else
- cursor.setBufferPosition(start)
-
- mutateSelection: (selection) ->
- {text, type} = @vimState.register.get(null, selection, @sequentialPaste)
- unless text
- @cancelled = true
- return
-
- text = _.multiplyString(text, @getCount())
- @linewisePaste = type is 'linewise' or @isMode('visual', 'linewise')
- newRange = @paste(selection, text, {@linewisePaste})
- @mutationsBySelection.set(selection, newRange)
- @vimState.sequentialPasteManager.savePastedRangeForSelection(selection, newRange)
-
- paste: (selection, text, {linewisePaste}) ->
- if @sequentialPaste
- @pasteCharacterwise(selection, text)
- else if linewisePaste
- @pasteLinewise(selection, text)
- else
- @pasteCharacterwise(selection, text)
-
- pasteCharacterwise: (selection, text) ->
- {cursor} = selection
- if selection.isEmpty() and @location is 'after' and not isEmptyRow(@editor, cursor.getBufferRow())
- cursor.moveRight()
- return selection.insertText(text)
-
- # Return newRange
- pasteLinewise: (selection, text) ->
- {cursor} = selection
- cursorRow = cursor.getBufferRow()
- text += "\n" unless text.endsWith("\n")
- if selection.isEmpty()
- if @location is 'before'
- insertTextAtBufferPosition(@editor, [cursorRow, 0], text)
- else if @location is 'after'
- targetRow = @getFoldEndRowForRow(cursorRow)
- ensureEndsWithNewLineForBufferRow(@editor, targetRow)
- insertTextAtBufferPosition(@editor, [targetRow + 1, 0], text)
- else
- selection.insertText("\n") unless @isMode('visual', 'linewise')
- selection.insertText(text)
-
-class PutAfter extends PutBefore
- @extend()
- location: 'after'
-
-class PutBeforeWithAutoIndent extends PutBefore
- @extend()
-
- pasteLinewise: (selection, text) ->
- newRange = super
- adjustIndentWithKeepingLayout(@editor, newRange)
- return newRange
-
-class PutAfterWithAutoIndent extends PutBeforeWithAutoIndent
- @extend()
- location: 'after'
-
-class AddBlankLineBelow extends Operator
- @extend()
- flashTarget: false
- target: "Empty"
- stayAtSamePosition: true
- stayByMarker: true
- where: 'below'
-
- mutateSelection: (selection) ->
- row = selection.getHeadBufferPosition().row
- row += 1 if @where is 'below'
- point = [row, 0]
- @editor.setTextInBufferRange([point, point], "\n".repeat(@getCount()))
-
-class AddBlankLineAbove extends AddBlankLineBelow
- @extend()
- where: 'above'
diff --git a/lib/operator.js b/lib/operator.js
new file mode 100644
index 000000000..1490c5557
--- /dev/null
+++ b/lib/operator.js
@@ -0,0 +1,862 @@
+"use babel"
+
+const _ = require("underscore-plus")
+const {
+ isEmptyRow,
+ getWordPatternAtBufferPosition,
+ getSubwordPatternAtBufferPosition,
+ insertTextAtBufferPosition,
+ setBufferRow,
+ moveCursorToFirstCharacterAtRow,
+ ensureEndsWithNewLineForBufferRow,
+ adjustIndentWithKeepingLayout,
+ isSingleLineText,
+} = require("./utils")
+const Base = require("./base")
+
+class Operator extends Base {
+ static operationKind = "operator"
+ requireTarget = true
+ recordable = true
+
+ wise = null
+ occurrence = false
+ occurrenceType = "base"
+
+ flashTarget = true
+ flashCheckpoint = "did-finish"
+ flashType = "operator"
+ flashTypeForOccurrence = "operator-occurrence"
+ trackChange = false
+
+ patternForOccurrence = null
+ stayAtSamePosition = null
+ stayOptionName = null
+ stayByMarker = false
+ restorePositions = true
+ setToFirstCharacterOnLinewise = false
+
+ acceptPresetOccurrence = true
+ acceptPersistentSelection = true
+
+ bufferCheckpointByPurpose = null
+ mutateSelectionOrderd = false
+
+ // Experimentaly allow selectTarget before input Complete
+ // -------------------------
+ supportEarlySelect = false
+ targetSelected = null
+
+ canEarlySelect() {
+ return this.supportEarlySelect && !this.repeated
+ }
+ // -------------------------
+
+ // Called when operation finished
+ // This is essentially to reset state for `.` repeat.
+ resetState() {
+ this.targetSelected = null
+ this.occurrenceSelected = false
+ }
+
+ // Two checkpoint for different purpose
+ // - one for undo(handled by modeManager)
+ // - one for preserve last inserted text
+ createBufferCheckpoint(purpose) {
+ if (!this.bufferCheckpointByPurpose) this.bufferCheckpointByPurpose = {}
+ this.bufferCheckpointByPurpose[purpose] = this.editor.createCheckpoint()
+ }
+
+ getBufferCheckpoint(purpose) {
+ if (this.bufferCheckpointByPurpose) {
+ return this.bufferCheckpointByPurpose[purpose]
+ }
+ }
+
+ deleteBufferCheckpoint(purpose) {
+ if (this.bufferCheckpointByPurpose) {
+ delete this.bufferCheckpointByPurpose[purpose]
+ }
+ }
+
+ groupChangesSinceBufferCheckpoint(purpose) {
+ const checkpoint = this.getBufferCheckpoint(purpose)
+ if (checkpoint) {
+ this.editor.groupChangesSinceCheckpoint(checkpoint)
+ this.deleteBufferCheckpoint(purpose)
+ }
+ }
+
+ setMarkForChange(range) {
+ this.vimState.mark.set("[", range.start)
+ this.vimState.mark.set("]", range.end)
+ }
+
+ needFlash() {
+ return (
+ this.flashTarget &&
+ this.getConfig("flashOnOperate") &&
+ !this.getConfig("flashOnOperateBlacklist").includes(this.name) &&
+ (this.mode !== "visual" || this.submode !== this.target.wise) // e.g. Y in vC
+ )
+ }
+
+ flashIfNecessary(ranges) {
+ if (this.needFlash()) {
+ this.vimState.flash(ranges, {type: this.getFlashType()})
+ }
+ }
+
+ flashChangeIfNecessary() {
+ if (this.needFlash()) {
+ this.onDidFinishOperation(() => {
+ const ranges = this.mutationManager.getSelectedBufferRangesForCheckpoint(this.flashCheckpoint)
+ this.vimState.flash(ranges, {type: this.getFlashType()})
+ })
+ }
+ }
+
+ getFlashType() {
+ return this.occurrenceSelected ? this.flashTypeForOccurrence : this.flashType
+ }
+
+ trackChangeIfNecessary() {
+ if (!this.trackChange) return
+ this.onDidFinishOperation(() => {
+ const range = this.mutationManager.getMutatedBufferRangeForSelection(this.editor.getLastSelection())
+ if (range) this.setMarkForChange(range)
+ })
+ }
+
+ initialize() {
+ this.subscribeResetOccurrencePatternIfNeeded()
+ this.onDidSetOperatorModifier(options => this.setModifier(options))
+
+ // When preset-occurrence was exists, operate on occurrence-wise
+ if (this.acceptPresetOccurrence && this.occurrenceManager.hasMarkers()) {
+ this.occurrence = true
+ }
+
+ // [FIXME] ORDER-MATTER
+ // To pick cursor-word to find occurrence base pattern.
+ // This has to be done BEFORE converting persistent-selection into real-selection.
+ // Since when persistent-selection is actuall selected, it change cursor position.
+ if (this.occurrence && !this.occurrenceManager.hasMarkers()) {
+ const regex = this.patternForOccurrence || this.getPatternForOccurrenceType(this.occurrenceType)
+ this.occurrenceManager.addPattern(regex)
+ }
+
+ // This change cursor position.
+ if (this.selectPersistentSelectionIfNecessary()) {
+ // [FIXME] selection-wise is not synched if it already visual-mode
+ if (this.mode !== "visual") {
+ this.vimState.modeManager.activate("visual", this.swrap.detectWise(this.editor))
+ }
+ }
+
+ if (this.mode === "visual" && this.requireTarget) {
+ this.target = "CurrentSelection"
+ }
+ if (_.isString(this.target)) {
+ this.setTarget(this.getInstance(this.target))
+ }
+
+ return super.initialize()
+ }
+
+ subscribeResetOccurrencePatternIfNeeded() {
+ // [CAUTION]
+ // This method has to be called in PROPER timing.
+ // If occurrence is true but no preset-occurrence
+ // Treat that `occurrence` is BOUNDED to operator itself, so cleanp at finished.
+ if (this.occurrence && !this.occurrenceManager.hasMarkers()) {
+ this.onDidResetOperationStack(() => this.occurrenceManager.resetPatterns())
+ }
+ }
+
+ setModifier({wise, occurrence, occurrenceType}) {
+ if (wise) {
+ this.wise = wise
+ } else if (occurrence) {
+ this.occurrence = occurrence
+ this.occurrenceType = occurrenceType
+ // This is o modifier case(e.g. `c o p`, `d O f`)
+ // We RESET existing occurence-marker when `o` or `O` modifier is typed by user.
+ const regex = this.getPatternForOccurrenceType(occurrenceType)
+ this.occurrenceManager.addPattern(regex, {reset: true, occurrenceType})
+ this.onDidResetOperationStack(() => this.occurrenceManager.resetPatterns())
+ }
+ }
+
+ // return true/false to indicate success
+ selectPersistentSelectionIfNecessary() {
+ if (
+ this.acceptPersistentSelection &&
+ this.getConfig("autoSelectPersistentSelectionOnOperate") &&
+ !this.persistentSelection.isEmpty()
+ ) {
+ this.persistentSelection.select()
+ this.editor.mergeIntersectingSelections()
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ if (!$selection.hasProperties()) $selection.saveProperties()
+ }
+
+ return true
+ } else {
+ return false
+ }
+ }
+
+ getPatternForOccurrenceType(occurrenceType) {
+ if (occurrenceType === "base") {
+ return getWordPatternAtBufferPosition(this.editor, this.getCursorBufferPosition())
+ } else if (occurrenceType === "subword") {
+ return getSubwordPatternAtBufferPosition(this.editor, this.getCursorBufferPosition())
+ }
+ }
+
+ // target is TextObject or Motion to operate on.
+ setTarget(target) {
+ this.target = target
+ this.target.operator = this
+ this.emitDidSetTarget(this)
+
+ if (this.canEarlySelect()) {
+ this.normalizeSelectionsIfNecessary()
+ this.createBufferCheckpoint("undo")
+ this.selectTarget()
+ }
+ return this
+ }
+
+ setTextToRegisterForSelection(selection) {
+ this.setTextToRegister(selection.getText(), selection)
+ }
+
+ setTextToRegister(text, selection) {
+ if (this.target.isLinewise() && !text.endsWith("\n")) {
+ text += "\n"
+ }
+ if (text) {
+ this.vimState.register.set(null, {text, selection})
+
+ if (this.vimState.register.isUnnamed()) {
+ if (this.instanceof("Delete") || this.instanceof("Change")) {
+ if (!this.needSaveToNumberedRegister(this.target) && isSingleLineText(text)) {
+ this.vimState.register.set("-", {text, selection}) // small-change
+ } else {
+ this.vimState.register.set("1", {text, selection})
+ }
+ } else if (this.instanceof("Yank")) {
+ this.vimState.register.set("0", {text, selection})
+ }
+ }
+ }
+ }
+
+ needSaveToNumberedRegister(target) {
+ // Used to determine what register to use on change and delete operation.
+ // Following motion should save to 1-9 register regerdless of content is small or big.
+ const goesToNumberedRegisterMotionNames = [
+ "MoveToPair", // %
+ "MoveToNextSentence", // (, )
+ "Search", // /, ?, n, N
+ "MoveToNextParagraph", // {, }
+ ]
+ return goesToNumberedRegisterMotionNames.some(name => target.instanceof(name))
+ }
+
+ normalizeSelectionsIfNecessary() {
+ if (this.target && this.target.isMotion() && this.mode === "visual") {
+ this.swrap.normalize(this.editor)
+ }
+ }
+
+ startMutation(fn) {
+ if (this.canEarlySelect()) {
+ // - Skip selection normalization: already normalized before @selectTarget()
+ // - Manual checkpoint grouping: to create checkpoint before @selectTarget()
+ fn()
+ this.emitWillFinishMutation()
+ this.groupChangesSinceBufferCheckpoint("undo")
+ } else {
+ this.normalizeSelectionsIfNecessary()
+ this.editor.transact(() => {
+ fn()
+ this.emitWillFinishMutation()
+ })
+ }
+
+ this.emitDidFinishMutation()
+ }
+
+ // Main
+ execute() {
+ this.startMutation(() => {
+ if (this.selectTarget()) {
+ const selections = this.mutateSelectionOrderd
+ ? this.editor.getSelectionsOrderedByBufferPosition()
+ : this.editor.getSelections()
+
+ for (const selection of selections) {
+ this.mutateSelection(selection)
+ }
+ this.mutationManager.setCheckpoint("did-finish")
+ this.restoreCursorPositionsIfNecessary()
+ }
+ })
+
+ // Even though we fail to select target and fail to mutate,
+ // we have to return to normal-mode from operator-pending or visual
+ this.activateMode("normal")
+ }
+
+ // Return true unless all selection is empty.
+ selectTarget() {
+ if (this.targetSelected != null) {
+ return this.targetSelected
+ }
+ this.mutationManager.init({stayByMarker: this.stayByMarker})
+
+ if (this.target.isMotion() && this.mode === "visual") this.target.wise = this.submode
+ if (this.wise != null) this.target.forceWise(this.wise)
+
+ this.emitWillSelectTarget()
+
+ // Allow cursor position adjustment 'on-will-select-target' hook.
+ // so checkpoint comes AFTER @emitWillSelectTarget()
+ this.mutationManager.setCheckpoint("will-select")
+
+ // NOTE
+ // Since MoveToNextOccurrence, MoveToPreviousOccurrence motion move by
+ // occurrence-marker, occurrence-marker has to be created BEFORE `@target.execute()`
+ // And when repeated, occurrence pattern is already cached at @patternForOccurrence
+ if (this.repeated && this.occurrence && !this.occurrenceManager.hasMarkers()) {
+ this.occurrenceManager.addPattern(this.patternForOccurrence, {occurrenceType: this.occurrenceType})
+ }
+
+ this.target.execute()
+
+ this.mutationManager.setCheckpoint("did-select")
+ if (this.occurrence) {
+ // To repoeat(`.`) operation where multiple occurrence patterns was set.
+ // Here we save patterns which represent unioned regex which @occurrenceManager knows.
+ if (!this.patternForOccurrence) {
+ this.patternForOccurrence = this.occurrenceManager.buildPattern()
+ }
+
+ this.occurrenceWise = this.wise || "characterwise"
+ if (this.occurrenceManager.select(this.occurrenceWise)) {
+ this.occurrenceSelected = true
+ this.mutationManager.setCheckpoint("did-select-occurrence")
+ }
+ }
+
+ this.targetSelected = this.vimState.haveSomeNonEmptySelection() || this.target.name === "Empty"
+ if (this.targetSelected) {
+ this.emitDidSelectTarget()
+ this.flashChangeIfNecessary()
+ this.trackChangeIfNecessary()
+ } else {
+ this.emitDidFailSelectTarget()
+ }
+
+ return this.targetSelected
+ }
+
+ restoreCursorPositionsIfNecessary() {
+ if (!this.restorePositions) return
+
+ const stay =
+ this.stayAtSamePosition != null
+ ? this.stayAtSamePosition
+ : this.getConfig(this.stayOptionName) || (this.occurrenceSelected && this.getConfig("stayOnOccurrence"))
+ const wise = this.occurrenceSelected ? this.occurrenceWise : this.target.wise
+ const {setToFirstCharacterOnLinewise} = this
+ this.mutationManager.restoreCursorPositions({stay, wise, setToFirstCharacterOnLinewise})
+ }
+}
+Operator.register(false)
+
+class SelectBase extends Operator {
+ flashTarget = false
+ recordable = false
+
+ execute() {
+ this.startMutation(() => this.selectTarget())
+
+ if (this.target.selectSucceeded) {
+ if (this.target.isTextObject()) {
+ this.editor.scrollToCursorPosition()
+ }
+ const wise = this.occurrenceSelected ? this.occurrenceWise : this.target.wise
+ this.activateModeIfNecessary("visual", wise)
+ } else {
+ this.cancelOperation()
+ }
+ }
+}
+SelectBase.register(false)
+
+class Select extends SelectBase {
+ execute() {
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ if (!$selection.hasProperties()) $selection.saveProperties()
+ }
+ super.execute()
+ }
+}
+Select.register()
+
+class SelectLatestChange extends SelectBase {
+ target = "ALatestChange"
+}
+SelectLatestChange.register()
+
+class SelectPreviousSelection extends SelectBase {
+ target = "PreviousSelection"
+}
+SelectPreviousSelection.register()
+
+class SelectPersistentSelection extends SelectBase {
+ target = "APersistentSelection"
+ acceptPersistentSelection = false
+}
+SelectPersistentSelection.register()
+
+class SelectOccurrence extends SelectBase {
+ occurrence = true
+}
+SelectOccurrence.register()
+
+// SelectInVisualMode: used in visual-mode
+// When text-object is invoked from normal or viusal-mode, operation would be
+// => SelectInVisualMode operator with target=text-object
+// When motion is invoked from visual-mode, operation would be
+// => SelectInVisualMode operator with target=motion)
+// ================================
+// SelectInVisualMode is used in TWO situation.
+// - visual-mode operation
+// - e.g: `v l`, `V j`, `v i p`...
+// - Directly invoke text-object from normal-mode
+// - e.g: Invoke `Inner Paragraph` from command-palette.
+class SelectInVisualMode extends SelectBase {
+ acceptPresetOccurrence = false
+ acceptPersistentSelection = false
+}
+SelectInVisualMode.register(false)
+
+// Persistent Selection
+// =========================
+class CreatePersistentSelection extends Operator {
+ flashTarget = false
+ stayAtSamePosition = true
+ acceptPresetOccurrence = false
+ acceptPersistentSelection = false
+
+ mutateSelection(selection) {
+ this.persistentSelection.markBufferRange(selection.getBufferRange())
+ }
+}
+CreatePersistentSelection.register()
+
+class TogglePersistentSelection extends CreatePersistentSelection {
+ isComplete() {
+ const point = this.editor.getCursorBufferPosition()
+ this.markerToRemove = this.persistentSelection.getMarkerAtPoint(point)
+ return this.markerToRemove || super.isComplete()
+ }
+
+ execute() {
+ if (this.markerToRemove) {
+ this.markerToRemove.destroy()
+ } else {
+ super.execute()
+ }
+ }
+}
+TogglePersistentSelection.register()
+
+// Preset Occurrence
+// =========================
+class TogglePresetOccurrence extends Operator {
+ target = "Empty"
+ flashTarget = false
+ acceptPresetOccurrence = false
+ acceptPersistentSelection = false
+ occurrenceType = "base"
+
+ execute() {
+ const marker = this.occurrenceManager.getMarkerAtPoint(this.editor.getCursorBufferPosition())
+ if (marker) {
+ this.occurrenceManager.destroyMarkers([marker])
+ } else {
+ const isNarrowed = this.vimState.modeManager.isNarrowed()
+
+ let regex
+ if (this.mode === "visual" && !isNarrowed) {
+ this.occurrenceType = "base"
+ regex = new RegExp(_.escapeRegExp(this.editor.getSelectedText()), "g")
+ } else {
+ regex = this.getPatternForOccurrenceType(this.occurrenceType)
+ }
+
+ this.occurrenceManager.addPattern(regex, {occurrenceType: this.occurrenceType})
+ this.occurrenceManager.saveLastPattern(this.occurrenceType)
+
+ if (!isNarrowed) this.activateMode("normal")
+ }
+ }
+}
+TogglePresetOccurrence.register()
+
+class TogglePresetSubwordOccurrence extends TogglePresetOccurrence {
+ occurrenceType = "subword"
+}
+TogglePresetSubwordOccurrence.register()
+
+// Want to rename RestoreOccurrenceMarker
+class AddPresetOccurrenceFromLastOccurrencePattern extends TogglePresetOccurrence {
+ execute() {
+ this.occurrenceManager.resetPatterns()
+ const regex = this.globalState.get("lastOccurrencePattern")
+ if (regex) {
+ const occurrenceType = this.globalState.get("lastOccurrenceType")
+ this.occurrenceManager.addPattern(regex, {occurrenceType})
+ this.activateMode("normal")
+ }
+ }
+}
+AddPresetOccurrenceFromLastOccurrencePattern.register()
+
+// Delete
+// ================================
+class Delete extends Operator {
+ trackChange = true
+ flashCheckpoint = "did-select-occurrence"
+ flashTypeForOccurrence = "operator-remove-occurrence"
+ stayOptionName = "stayOnDelete"
+ setToFirstCharacterOnLinewise = true
+
+ execute() {
+ this.onDidSelectTarget(() => {
+ if (this.occurrenceSelected && this.occurrenceWise === "linewise") {
+ this.flashTarget = false
+ }
+ })
+
+ if (this.target.wise === "blockwise") {
+ this.restorePositions = false
+ }
+ super.execute()
+ }
+
+ mutateSelection(selection) {
+ this.setTextToRegisterForSelection(selection)
+ selection.deleteSelectedText()
+ }
+}
+Delete.register()
+
+class DeleteRight extends Delete {
+ target = "MoveRight"
+}
+DeleteRight.register()
+
+class DeleteLeft extends Delete {
+ target = "MoveLeft"
+}
+DeleteLeft.register()
+
+class DeleteToLastCharacterOfLine extends Delete {
+ target = "MoveToLastCharacterOfLine"
+
+ execute() {
+ this.onDidSelectTarget(() => {
+ if (this.target.wise === "blockwise") {
+ for (const blockwiseSelection of this.getBlockwiseSelections()) {
+ blockwiseSelection.extendMemberSelectionsToEndOfLine()
+ }
+ }
+ })
+ super.execute()
+ }
+}
+DeleteToLastCharacterOfLine.register()
+
+class DeleteLine extends Delete {
+ wise = "linewise"
+ target = "MoveToRelativeLine"
+ flashTarget = false
+}
+DeleteLine.register()
+
+// Yank
+// =========================
+class Yank extends Operator {
+ trackChange = true
+ stayOptionName = "stayOnYank"
+
+ mutateSelection(selection) {
+ this.setTextToRegisterForSelection(selection)
+ }
+}
+Yank.register()
+
+class YankLine extends Yank {
+ wise = "linewise"
+ target = "MoveToRelativeLine"
+}
+YankLine.register()
+
+class YankToLastCharacterOfLine extends Yank {
+ target = "MoveToLastCharacterOfLine"
+}
+YankToLastCharacterOfLine.register()
+
+// -------------------------
+// [ctrl-a]
+class Increase extends Operator {
+ target = "Empty" // ctrl-a in normal-mode find target number in current line manually
+ flashTarget = false // do manually
+ restorePositions = false // do manually
+ step = 1
+
+ execute() {
+ this.newRanges = []
+ if (!this.regex) this.regex = new RegExp(`${this.getConfig("numberRegex")}`, "g")
+
+ super.execute()
+
+ if (this.newRanges.length) {
+ if (this.getConfig("flashOnOperate") && !this.getConfig("flashOnOperateBlacklist").includes(this.name)) {
+ this.vimState.flash(this.newRanges, {type: this.flashTypeForOccurrence})
+ }
+ }
+ }
+
+ replaceNumberInBufferRange(scanRange, fn) {
+ const newRanges = []
+ this.scanForward(this.regex, {scanRange}, event => {
+ if (fn) {
+ if (fn(event)) event.stop()
+ else return
+ }
+ const nextNumber = this.getNextNumber(event.matchText)
+ newRanges.push(event.replace(String(nextNumber)))
+ })
+ return newRanges
+ }
+
+ mutateSelection(selection) {
+ const {cursor} = selection
+ if (this.target.is("Empty")) {
+ // ctrl-a, ctrl-x in `normal-mode`
+ const cursorPosition = cursor.getBufferPosition()
+ const scanRange = this.editor.bufferRangeForBufferRow(cursorPosition.row)
+ const newRanges = this.replaceNumberInBufferRange(scanRange, event =>
+ event.range.end.isGreaterThan(cursorPosition)
+ )
+ const point = (newRanges.length && newRanges[0].end.translate([0, -1])) || cursorPosition
+ cursor.setBufferPosition(point)
+ } else {
+ const scanRange = selection.getBufferRange()
+ this.newRanges.push(...this.replaceNumberInBufferRange(scanRange))
+ cursor.setBufferPosition(scanRange.start)
+ }
+ }
+
+ getNextNumber(numberString) {
+ return Number.parseInt(numberString, 10) + this.step * this.getCount()
+ }
+}
+Increase.register()
+
+// [ctrl-x]
+class Decrease extends Increase {
+ step = -1
+}
+Decrease.register()
+
+// -------------------------
+// [g ctrl-a]
+class IncrementNumber extends Increase {
+ baseNumber = null
+ target = null
+ mutateSelectionOrderd = true
+
+ getNextNumber(numberString) {
+ if (this.baseNumber != null) {
+ this.baseNumber += this.step * this.getCount()
+ } else {
+ this.baseNumber = Number.parseInt(numberString, 10)
+ }
+ return this.baseNumber
+ }
+}
+IncrementNumber.register()
+
+// [g ctrl-x]
+class DecrementNumber extends IncrementNumber {
+ step = -1
+}
+DecrementNumber.register()
+
+// Put
+// -------------------------
+// Cursor placement:
+// - place at end of mutation: paste non-multiline characterwise text
+// - place at start of mutation: non-multiline characterwise text(characterwise, linewise)
+class PutBefore extends Operator {
+ location = "before"
+ target = "Empty"
+ flashType = "operator-long"
+ restorePositions = false // manage manually
+ flashTarget = false // manage manually
+ trackChange = false // manage manually
+
+ initialize() {
+ this.vimState.sequentialPasteManager.onInitialize(this)
+ return super.initialize()
+ }
+
+ execute() {
+ this.mutationsBySelection = new Map()
+ this.sequentialPaste = this.vimState.sequentialPasteManager.onExecute(this)
+
+ this.onDidFinishMutation(() => {
+ if (!this.cancelled) this.adjustCursorPosition()
+ })
+
+ super.execute()
+
+ if (this.cancelled) return
+
+ this.onDidFinishOperation(() => {
+ // TrackChange
+ const newRange = this.mutationsBySelection.get(this.editor.getLastSelection())
+ if (newRange) this.setMarkForChange(newRange)
+
+ // Flash
+ if (this.getConfig("flashOnOperate") && !this.getConfig("flashOnOperateBlacklist").includes(this.name)) {
+ const ranges = this.editor.getSelections().map(selection => this.mutationsBySelection.get(selection))
+ this.vimState.flash(ranges, {type: this.getFlashType()})
+ }
+ })
+ }
+
+ adjustCursorPosition() {
+ for (const selection of this.editor.getSelections()) {
+ if (!this.mutationsBySelection.has(selection)) continue
+
+ const {cursor} = selection
+ const newRange = this.mutationsBySelection.get(selection)
+ if (this.linewisePaste) {
+ moveCursorToFirstCharacterAtRow(cursor, newRange.start.row)
+ } else {
+ if (newRange.isSingleLine()) {
+ cursor.setBufferPosition(newRange.end.translate([0, -1]))
+ } else {
+ cursor.setBufferPosition(newRange.start)
+ }
+ }
+ }
+ }
+
+ mutateSelection(selection) {
+ const value = this.vimState.register.get(null, selection, this.sequentialPaste)
+ if (!value.text) {
+ this.cancelled = true
+ return
+ }
+
+ const textToPaste = _.multiplyString(value.text, this.getCount())
+ this.linewisePaste = value.type === "linewise" || this.isMode("visual", "linewise")
+ const newRange = this.paste(selection, textToPaste, {linewisePaste: this.linewisePaste})
+ this.mutationsBySelection.set(selection, newRange)
+ this.vimState.sequentialPasteManager.savePastedRangeForSelection(selection, newRange)
+ }
+
+ // Return pasted range
+ paste(selection, text, {linewisePaste}) {
+ if (this.sequentialPaste) {
+ return this.pasteCharacterwise(selection, text)
+ } else if (linewisePaste) {
+ return this.pasteLinewise(selection, text)
+ } else {
+ return this.pasteCharacterwise(selection, text)
+ }
+ }
+
+ pasteCharacterwise(selection, text) {
+ const {cursor} = selection
+ if (selection.isEmpty() && this.location === "after" && !isEmptyRow(this.editor, cursor.getBufferRow())) {
+ cursor.moveRight()
+ }
+ return selection.insertText(text)
+ }
+
+ // Return newRange
+ pasteLinewise(selection, text) {
+ const {cursor} = selection
+ const cursorRow = cursor.getBufferRow()
+ if (!text.endsWith("\n")) {
+ text += "\n"
+ }
+ if (selection.isEmpty()) {
+ if (this.location === "before") {
+ return insertTextAtBufferPosition(this.editor, [cursorRow, 0], text)
+ } else if (this.location === "after") {
+ const targetRow = this.getFoldEndRowForRow(cursorRow)
+ ensureEndsWithNewLineForBufferRow(this.editor, targetRow)
+ return insertTextAtBufferPosition(this.editor, [targetRow + 1, 0], text)
+ }
+ } else {
+ if (!this.isMode("visual", "linewise")) {
+ selection.insertText("\n")
+ }
+ return selection.insertText(text)
+ }
+ }
+}
+PutBefore.register()
+
+class PutAfter extends PutBefore {
+ location = "after"
+}
+PutAfter.register()
+
+class PutBeforeWithAutoIndent extends PutBefore {
+ pasteLinewise(selection, text) {
+ const newRange = super.pasteLinewise(selection, text)
+ adjustIndentWithKeepingLayout(this.editor, newRange)
+ return newRange
+ }
+}
+PutBeforeWithAutoIndent.register()
+
+class PutAfterWithAutoIndent extends PutBeforeWithAutoIndent {
+ location = "after"
+}
+PutAfterWithAutoIndent.register()
+
+class AddBlankLineBelow extends Operator {
+ flashTarget = false
+ target = "Empty"
+ stayAtSamePosition = true
+ stayByMarker = true
+ where = "below"
+
+ mutateSelection(selection) {
+ const point = selection.getHeadBufferPosition()
+ if (this.where === "below") point.row++
+ point.column = 0
+ this.editor.setTextInBufferRange([point, point], "\n".repeat(this.getCount()))
+ }
+}
+AddBlankLineBelow.register()
+
+class AddBlankLineAbove extends AddBlankLineBelow {
+ where = "above"
+}
+AddBlankLineAbove.register()
diff --git a/lib/selection-wrapper.js b/lib/selection-wrapper.js
index 1b82a4590..9bd4e85ba 100644
--- a/lib/selection-wrapper.js
+++ b/lib/selection-wrapper.js
@@ -8,7 +8,7 @@ const {
isLinewiseRange,
assertWithException,
getFoldEndRowForRow,
- getRange,
+ getList,
} = require("./utils")
const settings = require("./settings")
const BlockwiseSelection = require("./blockwise-selection")
@@ -83,7 +83,7 @@ class SelectionWrapper {
getRows() {
const [startRow, endRow] = this.selection.getBufferRowRange()
- return getRange(startRow, endRow)
+ return getList(startRow, endRow)
}
getRowCount() {
diff --git a/lib/settings.js b/lib/settings.js
index cffc52f2d..658f48ca8 100644
--- a/lib/settings.js
+++ b/lib/settings.js
@@ -46,6 +46,20 @@ class Settings {
})
}
+ notifyCoffeeScriptNoLongerSupportedToExtendVMP() {
+ if (!this.get("notifiedCoffeeScriptNoLongerSupportedToExtendVMP")) {
+ this.set("notifiedCoffeeScriptNoLongerSupportedToExtendVMP", true)
+ const message = [
+ this.scope,
+ "- From vmp-v.1.9.0 all operations are defined as ES6 class which is NOT extend-able by CoffeeScript.",
+ "- If you have vmp custom operations in your `init.coffee`. Those are no longer work(You might saw error already).",
+ "- Sorry for not providing gradual migration path, I couldn't find the way and also I'm lazy.",
+ "- See [CHANGELOG](https://github.com/t9md/atom-vim-mode-plus/blob/master/CHANGELOG.md) and [Wiki](https://github.com/t9md/atom-vim-mode-plus/wiki/ExtendVimModePlusInInitFile) for detail.",
+ ].join("\n")
+ atom.notifications.addInfo(message, {dismissable: true})
+ }
+ }
+
migrateRenamedParams() {
const messages = []
@@ -475,6 +489,10 @@ module.exports = new Settings("vim-mode-plus", {
description:
"When attempt to create occurrence-marker exceeding this threshold, vmp asks confirmation to continue
This is to prevent editor from freezing while creating tons of markers.
Affects: `g o` or `o` modifier(e.g. `c o p`)",
},
+ notifiedCoffeeScriptNoLongerSupportedToExtendVMP: {
+ // TODO: Remove in future:(added at v1.19.0 release).
+ default: false,
+ },
debug: {
default: false,
description: "[Dev use]",
diff --git a/lib/text-object.coffee b/lib/text-object.coffee
deleted file mode 100644
index 8981c5ec7..000000000
--- a/lib/text-object.coffee
+++ /dev/null
@@ -1,710 +0,0 @@
-{Range, Point} = require 'atom'
-_ = require 'underscore-plus'
-
-# [TODO] Need overhaul
-# - [ ] Make expandable by selection.getBufferRange().union(@getRange(selection))
-# - [ ] Count support(priority low)?
-Base = require './base'
-{
- getLineTextToBufferPosition
- getCodeFoldRowRangesContainesForRow
- isIncludeFunctionScopeForRow
- expandRangeToWhiteSpaces
- getVisibleBufferRange
- translatePointAndClip
- getBufferRows
- getValidVimBufferRow
- trimRange
- sortRanges
- pointIsAtEndOfLine
- splitArguments
- traverseTextFromPoint
-} = require './utils'
-PairFinder = null
-
-class TextObject extends Base
- @extend(false)
- @operationKind: 'text-object'
- wise: 'characterwise'
- supportCount: false # FIXME #472, #66
- selectOnce: false
- selectSucceeded: false
-
- @deriveInnerAndA: ->
- @generateClass("A" + @name, false)
- @generateClass("Inner" + @name, true)
-
- @deriveInnerAndAForAllowForwarding: ->
- @generateClass("A" + @name + "AllowForwarding", false, true)
- @generateClass("Inner" + @name + "AllowForwarding", true, true)
-
- @generateClass: (klassName, inner, allowForwarding) ->
- klass = class extends this
- Object.defineProperty klass, 'name', get: -> klassName
- klass::inner = inner
- klass::allowForwarding = true if allowForwarding
- klass.extend()
-
- constructor: ->
- super
- @initialize()
-
- isInner: ->
- @inner
-
- isA: ->
- not @inner
-
- isLinewise: -> @wise is 'linewise'
- isBlockwise: -> @wise is 'blockwise'
-
- forceWise: (wise) ->
- @wise = wise # FIXME currently not well supported
-
- resetState: ->
- @selectSucceeded = false
-
- execute: ->
- # Whennever TextObject is executed, it has @operator
- # Called from Operator::selectTarget()
- # - `v i p`, is `SelectInVisualMode` operator with @target = `InnerParagraph`.
- # - `d i p`, is `Delete` operator with @target = `InnerParagraph`.
- if @operator?
- @select()
- else
- throw new Error('in TextObject: Must not happen')
-
- select: ->
- if @isMode('visual', 'blockwise')
- @swrap.normalize(@editor)
-
- @countTimes @getCount(), ({stop}) =>
- stop() unless @supportCount # quick-fix for #560
- for selection in @editor.getSelections()
- oldRange = selection.getBufferRange()
- if @selectTextObject(selection)
- @selectSucceeded = true
- stop() if selection.getBufferRange().isEqual(oldRange)
- break if @selectOnce
-
- @editor.mergeIntersectingSelections()
- # Some TextObject's wise is NOT deterministic. It has to be detected from selected range.
- @wise ?= @swrap.detectWise(@editor)
-
- if @operator.instanceof("SelectBase")
- if @selectSucceeded
- switch @wise
- when 'characterwise'
- $selection.saveProperties() for $selection in @swrap.getSelections(@editor)
- when 'linewise'
- # When target is persistent-selection, new selection is added after selectTextObject.
- # So we have to assure all selection have selction property.
- # Maybe this logic can be moved to operation stack.
- for $selection in @swrap.getSelections(@editor)
- if @getConfig('stayOnSelectTextObject')
- $selection.saveProperties() unless $selection.hasProperties()
- else
- $selection.saveProperties()
- $selection.fixPropertyRowToRowRange()
-
- if @submode is 'blockwise'
- for $selection in @swrap.getSelections(@editor)
- $selection.normalize()
- $selection.applyWise('blockwise')
-
- # Return true or false
- selectTextObject: (selection) ->
- if range = @getRange(selection)
- @swrap(selection).setBufferRange(range)
- return true
-
- # to override
- getRange: (selection) ->
- null
-
-# Section: Word
-# =========================
-class Word extends TextObject
- @extend(false)
- @deriveInnerAndA()
-
- getRange: (selection) ->
- point = @getCursorPositionForSelection(selection)
- {range} = @getWordBufferRangeAndKindAtBufferPosition(point, {@wordRegex})
- if @isA()
- expandRangeToWhiteSpaces(@editor, range)
- else
- range
-
-class WholeWord extends Word
- @extend(false)
- @deriveInnerAndA()
- wordRegex: /\S+/
-
-# Just include _, -
-class SmartWord extends Word
- @extend(false)
- @deriveInnerAndA()
- @description: "A word that consists of alphanumeric chars(`/[A-Za-z0-9_]/`) and hyphen `-`"
- wordRegex: /[\w-]+/
-
-# Just include _, -
-class Subword extends Word
- @extend(false)
- @deriveInnerAndA()
- getRange: (selection) ->
- @wordRegex = selection.cursor.subwordRegExp()
- super
-
-# Section: Pair
-# =========================
-class Pair extends TextObject
- @extend(false)
- supportCount: true
- allowNextLine: null
- adjustInnerRange: true
- pair: null
- inclusive: true
-
- initialize: ->
- PairFinder ?= require './pair-finder'
- super
-
-
- isAllowNextLine: ->
- @allowNextLine ? (@pair? and @pair[0] isnt @pair[1])
-
- adjustRange: ({start, end}) ->
- # Dirty work to feel natural for human, to behave compatible with pure Vim.
- # Where this adjustment appear is in following situation.
- # op-1: `ci{` replace only 2nd line
- # op-2: `di{` delete only 2nd line.
- # text:
- # {
- # aaa
- # }
- if pointIsAtEndOfLine(@editor, start)
- start = start.traverse([1, 0])
-
- if getLineTextToBufferPosition(@editor, end).match(/^\s*$/)
- if @mode is 'visual'
- # This is slightly innconsistent with regular Vim
- # - regular Vim: select new line after EOL
- # - vim-mode-plus: select to EOL(before new line)
- # This is intentional since to make submode `characterwise` when auto-detect submode
- # innerEnd = new Point(innerEnd.row - 1, Infinity)
- end = new Point(end.row - 1, Infinity)
- else
- end = new Point(end.row, 0)
-
- new Range(start, end)
-
- getFinder: ->
- options = {allowNextLine: @isAllowNextLine(), @allowForwarding, @pair, @inclusive}
- if @pair[0] is @pair[1]
- new PairFinder.QuoteFinder(@editor, options)
- else
- new PairFinder.BracketFinder(@editor, options)
-
- getPairInfo: (from) ->
- pairInfo = @getFinder().find(from)
- unless pairInfo?
- return null
- pairInfo.innerRange = @adjustRange(pairInfo.innerRange) if @adjustInnerRange
- pairInfo.targetRange = if @isInner() then pairInfo.innerRange else pairInfo.aRange
- pairInfo
-
- getRange: (selection) ->
- originalRange = selection.getBufferRange()
- pairInfo = @getPairInfo(@getCursorPositionForSelection(selection))
- # When range was same, try to expand range
- if pairInfo?.targetRange.isEqual(originalRange)
- pairInfo = @getPairInfo(pairInfo.aRange.end)
- pairInfo?.targetRange
-
-# Used by DeleteSurround
-class APair extends Pair
- @extend(false)
-
-class AnyPair extends Pair
- @extend(false)
- @deriveInnerAndA()
- allowForwarding: false
- member: [
- 'DoubleQuote', 'SingleQuote', 'BackTick',
- 'CurlyBracket', 'AngleBracket', 'SquareBracket', 'Parenthesis'
- ]
-
- getRanges: (selection) ->
- @member
- .map (klass) => @new(klass, {@inner, @allowForwarding, @inclusive}).getRange(selection)
- .filter (range) -> range?
-
- getRange: (selection) ->
- _.last(sortRanges(@getRanges(selection)))
-
-class AnyPairAllowForwarding extends AnyPair
- @extend(false)
- @deriveInnerAndA()
- @description: "Range surrounded by auto-detected paired chars from enclosed and forwarding area"
- allowForwarding: true
- getRange: (selection) ->
- ranges = @getRanges(selection)
- from = selection.cursor.getBufferPosition()
- [forwardingRanges, enclosingRanges] = _.partition ranges, (range) ->
- range.start.isGreaterThanOrEqual(from)
- enclosingRange = _.last(sortRanges(enclosingRanges))
- forwardingRanges = sortRanges(forwardingRanges)
-
- # When enclosingRange is exists,
- # We don't go across enclosingRange.end.
- # So choose from ranges contained in enclosingRange.
- if enclosingRange
- forwardingRanges = forwardingRanges.filter (range) ->
- enclosingRange.containsRange(range)
-
- forwardingRanges[0] or enclosingRange
-
-class AnyQuote extends AnyPair
- @extend(false)
- @deriveInnerAndA()
- allowForwarding: true
- member: ['DoubleQuote', 'SingleQuote', 'BackTick']
- getRange: (selection) ->
- ranges = @getRanges(selection)
- # Pick range which end.colum is leftmost(mean, closed first)
- _.first(_.sortBy(ranges, (r) -> r.end.column)) if ranges.length
-
-class Quote extends Pair
- @extend(false)
- allowForwarding: true
-
-class DoubleQuote extends Quote
- @extend(false)
- @deriveInnerAndA()
- pair: ['"', '"']
-
-class SingleQuote extends Quote
- @extend(false)
- @deriveInnerAndA()
- pair: ["'", "'"]
-
-class BackTick extends Quote
- @extend(false)
- @deriveInnerAndA()
- pair: ['`', '`']
-
-class CurlyBracket extends Pair
- @extend(false)
- @deriveInnerAndA()
- @deriveInnerAndAForAllowForwarding()
- pair: ['{', '}']
-
-class SquareBracket extends Pair
- @extend(false)
- @deriveInnerAndA()
- @deriveInnerAndAForAllowForwarding()
- pair: ['[', ']']
-
-class Parenthesis extends Pair
- @extend(false)
- @deriveInnerAndA()
- @deriveInnerAndAForAllowForwarding()
- pair: ['(', ')']
-
-class AngleBracket extends Pair
- @extend(false)
- @deriveInnerAndA()
- @deriveInnerAndAForAllowForwarding()
- pair: ['<', '>']
-
-class Tag extends Pair
- @extend(false)
- @deriveInnerAndA()
- allowNextLine: true
- allowForwarding: true
- adjustInnerRange: false
-
- getTagStartPoint: (from) ->
- tagRange = null
- pattern = PairFinder.TagFinder.pattern
- @scanForward pattern, {from: [from.row, 0]}, ({range, stop}) ->
- if range.containsPoint(from, true)
- tagRange = range
- stop()
- tagRange?.start
-
- getFinder: ->
- new PairFinder.TagFinder(@editor, {allowNextLine: @isAllowNextLine(), @allowForwarding, @inclusive})
-
- getPairInfo: (from) ->
- super(@getTagStartPoint(from) ? from)
-
-# Section: Paragraph
-# =========================
-# Paragraph is defined as consecutive (non-)blank-line.
-class Paragraph extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: 'linewise'
- supportCount: true
-
- findRow: (fromRow, direction, fn) ->
- fn.reset?()
- foundRow = fromRow
- for row in getBufferRows(@editor, {startRow: fromRow, direction})
- break unless fn(row, direction)
- foundRow = row
-
- foundRow
-
- findRowRangeBy: (fromRow, fn) ->
- startRow = @findRow(fromRow, 'previous', fn)
- endRow = @findRow(fromRow, 'next', fn)
- [startRow, endRow]
-
- getPredictFunction: (fromRow, selection) ->
- fromRowResult = @editor.isBufferRowBlank(fromRow)
-
- if @isInner()
- predict = (row, direction) =>
- @editor.isBufferRowBlank(row) is fromRowResult
- else
- if selection.isReversed()
- directionToExtend = 'previous'
- else
- directionToExtend = 'next'
-
- flip = false
- predict = (row, direction) =>
- result = @editor.isBufferRowBlank(row) is fromRowResult
- if flip
- not result
- else
- if (not result) and (direction is directionToExtend)
- flip = true
- return true
- result
-
- predict.reset = ->
- flip = false
- predict
-
- getRange: (selection) ->
- originalRange = selection.getBufferRange()
- fromRow = @getCursorPositionForSelection(selection).row
- if @isMode('visual', 'linewise')
- if selection.isReversed()
- fromRow--
- else
- fromRow++
- fromRow = getValidVimBufferRow(@editor, fromRow)
-
- rowRange = @findRowRangeBy(fromRow, @getPredictFunction(fromRow, selection))
- selection.getBufferRange().union(@getBufferRangeForRowRange(rowRange))
-
-class Indentation extends Paragraph
- @extend(false)
- @deriveInnerAndA()
-
- getRange: (selection) ->
- fromRow = @getCursorPositionForSelection(selection).row
-
- baseIndentLevel = @getIndentLevelForBufferRow(fromRow)
- predict = (row) =>
- if @editor.isBufferRowBlank(row)
- @isA()
- else
- @getIndentLevelForBufferRow(row) >= baseIndentLevel
-
- rowRange = @findRowRangeBy(fromRow, predict)
- @getBufferRangeForRowRange(rowRange)
-
-# Section: Comment
-# =========================
-class Comment extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: 'linewise'
-
- getRange: (selection) ->
- row = @getCursorPositionForSelection(selection).row
- rowRange = @editor.languageMode.rowRangeForCommentAtBufferRow(row)
- rowRange ?= [row, row] if @editor.isBufferRowCommented(row)
- if rowRange?
- @getBufferRangeForRowRange(rowRange)
-
-class CommentOrParagraph extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: 'linewise'
-
- getRange: (selection) ->
- for klass in ['Comment', 'Paragraph']
- if range = @new(klass, {@inner}).getRange(selection)
- return range
-
-# Section: Fold
-# =========================
-class Fold extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: 'linewise'
-
- adjustRowRange: (rowRange) ->
- return rowRange if @isA()
-
- [startRow, endRow] = rowRange
- if @getIndentLevelForBufferRow(startRow) is @getIndentLevelForBufferRow(endRow)
- endRow -= 1
- startRow += 1
- [startRow, endRow]
-
- getFoldRowRangesContainsForRow: (row) ->
- getCodeFoldRowRangesContainesForRow(@editor, row).reverse()
-
- getRange: (selection) ->
- row = @getCursorPositionForSelection(selection).row
- selectedRange = selection.getBufferRange()
- for rowRange in @getFoldRowRangesContainsForRow(row)
- range = @getBufferRangeForRowRange(@adjustRowRange(rowRange))
-
- # Don't change to `if range.containsRange(selectedRange, true)`
- # There is behavior diff when cursor is at beginning of line( column 0 ).
- unless selectedRange.containsRange(range)
- return range
-
-# NOTE: Function range determination is depending on fold.
-class Function extends Fold
- @extend(false)
- @deriveInnerAndA()
- # Some language don't include closing `}` into fold.
- scopeNamesOmittingEndRow: ['source.go', 'source.elixir']
-
- isGrammarNotFoldEndRow: ->
- {scopeName, packageName} = @editor.getGrammar()
- if scopeName in @scopeNamesOmittingEndRow
- true
- else
- # HACK: Rust have two package `language-rust` and `atom-language-rust`
- # language-rust don't fold ending `}`, but atom-language-rust does.
- scopeName is 'source.rust' and packageName is "language-rust"
-
- getFoldRowRangesContainsForRow: (row) ->
- (super).filter (rowRange) =>
- isIncludeFunctionScopeForRow(@editor, rowRange[0])
-
- adjustRowRange: (rowRange) ->
- [startRow, endRow] = super
- # NOTE: This adjustment shoud not be necessary if language-syntax is properly defined.
- if @isA() and @isGrammarNotFoldEndRow()
- endRow += 1
- [startRow, endRow]
-
-# Section: Other
-# =========================
-class Arguments extends TextObject
- @extend(false)
- @deriveInnerAndA()
-
- newArgInfo: (argStart, arg, separator) ->
- argEnd = traverseTextFromPoint(argStart, arg)
- argRange = new Range(argStart, argEnd)
-
- separatorEnd = traverseTextFromPoint(argEnd, separator ? '')
- separatorRange = new Range(argEnd, separatorEnd)
-
- innerRange = argRange
- aRange = argRange.union(separatorRange)
- {argRange, separatorRange, innerRange, aRange}
-
- getArgumentsRangeForSelection: (selection) ->
- member = [
- 'CurlyBracket'
- 'SquareBracket'
- 'Parenthesis'
- ]
- @new("InnerAnyPair", {inclusive: false, member: member}).getRange(selection)
-
- getRange: (selection) ->
- range = @getArgumentsRangeForSelection(selection)
- pairRangeFound = range?
- range ?= @new("InnerCurrentLine").getRange(selection) # fallback
- return unless range
-
- range = trimRange(@editor, range)
-
- text = @editor.getTextInBufferRange(range)
- allTokens = splitArguments(text, pairRangeFound)
-
- argInfos = []
- argStart = range.start
-
- # Skip starting separator
- if allTokens.length and allTokens[0].type is 'separator'
- token = allTokens.shift()
- argStart = traverseTextFromPoint(argStart, token.text)
-
- while allTokens.length
- token = allTokens.shift()
- if token.type is 'argument'
- separator = allTokens.shift()?.text
- argInfo = @newArgInfo(argStart, token.text, separator)
-
- if (allTokens.length is 0) and (lastArgInfo = _.last(argInfos))
- argInfo.aRange = argInfo.argRange.union(lastArgInfo.separatorRange)
-
- argStart = argInfo.aRange.end
- argInfos.push(argInfo)
- else
- throw new Error('must not happen')
-
- point = @getCursorPositionForSelection(selection)
- for {innerRange, aRange} in argInfos
- if innerRange.end.isGreaterThanOrEqual(point)
- return if @isInner() then innerRange else aRange
- null
-
-class CurrentLine extends TextObject
- @extend(false)
- @deriveInnerAndA()
-
- getRange: (selection) ->
- row = @getCursorPositionForSelection(selection).row
- range = @editor.bufferRangeForBufferRow(row)
- if @isA()
- range
- else
- trimRange(@editor, range)
-
-class Entire extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: 'linewise'
- selectOnce: true
-
- getRange: (selection) ->
- @editor.buffer.getRange()
-
-class Empty extends TextObject
- @extend(false)
- selectOnce: true
-
-class LatestChange extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: null
- selectOnce: true
- getRange: (selection) ->
- start = @vimState.mark.get('[')
- end = @vimState.mark.get(']')
- if start? and end?
- new Range(start, end)
-
-class SearchMatchForward extends TextObject
- @extend()
- backward: false
-
- findMatch: (fromPoint, pattern) ->
- fromPoint = translatePointAndClip(@editor, fromPoint, "forward") if (@mode is 'visual')
- found = null
- @scanForward pattern, {from: [fromPoint.row, 0]}, ({range, stop}) ->
- if range.end.isGreaterThan(fromPoint)
- found = range
- stop()
- {range: found, whichIsHead: 'end'}
-
- getRange: (selection) ->
- pattern = @globalState.get('lastSearchPattern')
- return unless pattern?
-
- fromPoint = selection.getHeadBufferPosition()
- {range, whichIsHead} = @findMatch(fromPoint, pattern)
- if range?
- @unionRangeAndDetermineReversedState(selection, range, whichIsHead)
-
- unionRangeAndDetermineReversedState: (selection, found, whichIsHead) ->
- if selection.isEmpty()
- found
- else
- head = found[whichIsHead]
- tail = selection.getTailBufferPosition()
-
- if @backward
- head = translatePointAndClip(@editor, head, 'forward') if tail.isLessThan(head)
- else
- head = translatePointAndClip(@editor, head, 'backward') if head.isLessThan(tail)
-
- @reversed = head.isLessThan(tail)
- new Range(tail, head).union(@swrap(selection).getTailBufferRange())
-
- selectTextObject: (selection) ->
- if range = @getRange(selection)
- @swrap(selection).setBufferRange(range, {reversed: @reversed ? @backward})
- return true
-
-class SearchMatchBackward extends SearchMatchForward
- @extend()
- backward: true
-
- findMatch: (fromPoint, pattern) ->
- fromPoint = translatePointAndClip(@editor, fromPoint, "backward") if (@mode is 'visual')
- found = null
- @scanBackward pattern, {from: [fromPoint.row, Infinity]}, ({range, stop}) ->
- if range.start.isLessThan(fromPoint)
- found = range
- stop()
- {range: found, whichIsHead: 'start'}
-
-# [Limitation: won't fix]: Selected range is not submode aware. always characterwise.
-# So even if original selection was vL or vB, selected range by this text-object
-# is always vC range.
-class PreviousSelection extends TextObject
- @extend()
- wise: null
- selectOnce: true
-
- selectTextObject: (selection) ->
- {properties, submode} = @vimState.previousSelection
- if properties? and submode?
- @wise = submode
- @swrap(@editor.getLastSelection()).selectByProperties(properties)
- return true
-
-class PersistentSelection extends TextObject
- @extend(false)
- @deriveInnerAndA()
- wise: null
- selectOnce: true
-
- selectTextObject: (selection) ->
- if @vimState.hasPersistentSelections()
- @vimState.persistentSelection.setSelectedBufferRanges()
- return true
-
-# Used only by ReplaceWithRegister and PutBefore and its' children.
-class LastPastedRange extends TextObject
- @extend(false)
- wise: null
- selectOnce: true
-
- selectTextObject: (selection) ->
- for selection in @editor.getSelections()
- range = @vimState.sequentialPasteManager.getPastedRangeForSelection(selection)
- selection.setBufferRange(range)
-
- return true
-
-class VisibleArea extends TextObject
- @extend(false)
- @deriveInnerAndA()
- selectOnce: true
-
- getRange: (selection) ->
- # [BUG?] Need translate to shilnk top and bottom to fit actual row.
- # The reason I need -2 at bottom is because of status bar?
- bufferRange = getVisibleBufferRange(@editor)
- if bufferRange.getRows() > @editor.getRowsPerPage()
- bufferRange.translate([+1, 0], [-3, 0])
- else
- bufferRange
diff --git a/lib/text-object.js b/lib/text-object.js
new file mode 100644
index 000000000..e3173e55f
--- /dev/null
+++ b/lib/text-object.js
@@ -0,0 +1,802 @@
+"use babel"
+
+const {Range, Point} = require("atom")
+const _ = require("underscore-plus")
+
+// [TODO] Need overhaul
+// - [ ] Make expandable by selection.getBufferRange().union(this.getRange(selection))
+// - [ ] Count support(priority low)?
+const Base = require("./base")
+const {
+ getLineTextToBufferPosition,
+ getCodeFoldRowRangesContainesForRow,
+ isIncludeFunctionScopeForRow,
+ expandRangeToWhiteSpaces,
+ getVisibleBufferRange,
+ translatePointAndClip,
+ getBufferRows,
+ getValidVimBufferRow,
+ trimRange,
+ sortRanges,
+ pointIsAtEndOfLine,
+ splitArguments,
+ traverseTextFromPoint,
+} = require("./utils")
+let PairFinder
+
+class TextObject extends Base {
+ static operationKind = "text-object"
+
+ wise = "characterwise"
+ supportCount = false // FIXME #472, #66
+ selectOnce = false
+ selectSucceeded = false
+
+ static register(isCommand, deriveInnerAndA, deriveInnerAndAForAllowForwarding) {
+ super.register(isCommand)
+
+ if (deriveInnerAndA) {
+ this.generateClass(`A${this.name}`, false)
+ this.generateClass(`Inner${this.name}`, true)
+ }
+
+ if (deriveInnerAndAForAllowForwarding) {
+ this.generateClass(`A${this.name}AllowForwarding`, false, true)
+ this.generateClass(`Inner${this.name}AllowForwarding`, true, true)
+ }
+ }
+
+ static generateClass(klassName, inner, allowForwarding) {
+ const klass = class extends this {
+ static get name() {
+ return klassName
+ }
+ constructor(vimState) {
+ super(vimState)
+ this.inner = inner
+ if (allowForwarding != null) this.allowForwarding = allowForwarding
+ }
+ }
+ klass.register()
+ }
+
+ isInner() {
+ return this.inner
+ }
+
+ isA() {
+ return !this.inner
+ }
+
+ isLinewise() {
+ return this.wise === "linewise"
+ }
+
+ isBlockwise() {
+ return this.wise === "blockwise"
+ }
+
+ forceWise(wise) {
+ return (this.wise = wise) // FIXME currently not well supported
+ }
+
+ resetState() {
+ this.selectSucceeded = false
+ }
+
+ // execute: Called from Operator::selectTarget()
+ // - `v i p`, is `SelectInVisualMode` operator with @target = `InnerParagraph`.
+ // - `d i p`, is `Delete` operator with @target = `InnerParagraph`.
+ execute() {
+ // Whennever TextObject is executed, it has @operator
+ if (!this.operator) throw new Error("in TextObject: Must not happen")
+ this.select()
+ }
+
+ select() {
+ if (this.isMode("visual", "blockwise")) {
+ this.swrap.normalize(this.editor)
+ }
+
+ this.countTimes(this.getCount(), ({stop}) => {
+ if (!this.supportCount) stop() // quick-fix for #560
+
+ for (const selection of this.editor.getSelections()) {
+ const oldRange = selection.getBufferRange()
+ if (this.selectTextObject(selection)) this.selectSucceeded = true
+ if (selection.getBufferRange().isEqual(oldRange)) stop()
+ if (this.selectOnce) break
+ }
+ })
+
+ this.editor.mergeIntersectingSelections()
+ // Some TextObject's wise is NOT deterministic. It has to be detected from selected range.
+ if (this.wise == null) this.wise = this.swrap.detectWise(this.editor)
+
+ if (this.operator.instanceof("SelectBase")) {
+ if (this.selectSucceeded) {
+ if (this.wise === "characterwise") {
+ this.swrap.getSelections(this.editor).forEach($selection => $selection.saveProperties())
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ $selection.saveProperties()
+ }
+ } else if (this.wise === "linewise") {
+ // When target is persistent-selection, new selection is added after selectTextObject.
+ // So we have to assure all selection have selction property.
+ // Maybe this logic can be moved to operation stack.
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ if (this.getConfig("stayOnSelectTextObject")) {
+ if (!$selection.hasProperties()) $selection.saveProperties()
+ } else {
+ $selection.saveProperties()
+ }
+ $selection.fixPropertyRowToRowRange()
+ }
+ }
+ }
+
+ if (this.submode === "blockwise") {
+ for (const $selection of this.swrap.getSelections(this.editor)) {
+ $selection.normalize()
+ $selection.applyWise("blockwise")
+ }
+ }
+ }
+ }
+
+ // Return true or false
+ selectTextObject(selection) {
+ const range = this.getRange(selection)
+ if (range) {
+ this.swrap(selection).setBufferRange(range)
+ return true
+ } else {
+ return false
+ }
+ }
+
+ // to override
+ getRange(selection) {}
+}
+TextObject.register(false)
+
+// Section: Word
+// =========================
+class Word extends TextObject {
+ getRange(selection) {
+ const point = this.getCursorPositionForSelection(selection)
+ const {range} = this.getWordBufferRangeAndKindAtBufferPosition(point, {wordRegex: this.wordRegex})
+ return this.isA() ? expandRangeToWhiteSpaces(this.editor, range) : range
+ }
+}
+Word.register(false, true)
+
+class WholeWord extends Word {
+ wordRegex = /\S+/
+}
+WholeWord.register(false, true)
+
+// Just include _, -
+class SmartWord extends Word {
+ wordRegex = /[\w-]+/
+}
+SmartWord.register(false, true)
+
+// Just include _, -
+class Subword extends Word {
+ getRange(selection) {
+ this.wordRegex = selection.cursor.subwordRegExp()
+ return super.getRange(selection)
+ }
+}
+Subword.register(false, true)
+
+// Section: Pair
+// =========================
+class Pair extends TextObject {
+ supportCount = true
+ allowNextLine = null
+ adjustInnerRange = true
+ pair = null
+ inclusive = true
+
+ constructor(...args) {
+ super(...args)
+ if (!PairFinder) PairFinder = require("./pair-finder")
+ }
+
+ isAllowNextLine() {
+ return this.allowNextLine != null ? this.allowNextLine : this.pair != null && this.pair[0] !== this.pair[1]
+ }
+
+ adjustRange({start, end}) {
+ // Dirty work to feel natural for human, to behave compatible with pure Vim.
+ // Where this adjustment appear is in following situation.
+ // op-1: `ci{` replace only 2nd line
+ // op-2: `di{` delete only 2nd line.
+ // text:
+ // {
+ // aaa
+ // }
+ if (pointIsAtEndOfLine(this.editor, start)) {
+ start = start.traverse([1, 0])
+ }
+
+ if (getLineTextToBufferPosition(this.editor, end).match(/^\s*$/)) {
+ if (this.mode === "visual") {
+ // This is slightly innconsistent with regular Vim
+ // - regular Vim: select new line after EOL
+ // - vim-mode-plus: select to EOL(before new line)
+ // This is intentional since to make submode `characterwise` when auto-detect submode
+ // innerEnd = new Point(innerEnd.row - 1, Infinity)
+ end = new Point(end.row - 1, Infinity)
+ } else {
+ end = new Point(end.row, 0)
+ }
+ }
+ return new Range(start, end)
+ }
+
+ getFinder() {
+ const finderName = this.pair[0] === this.pair[1] ? "QuoteFinder" : "BracketFinder"
+ return new PairFinder[finderName](this.editor, {
+ allowNextLine: this.isAllowNextLine(),
+ allowForwarding: this.allowForwarding,
+ pair: this.pair,
+ inclusive: this.inclusive,
+ })
+ }
+
+ getPairInfo(from) {
+ const pairInfo = this.getFinder().find(from)
+ if (pairInfo) {
+ if (this.adjustInnerRange) pairInfo.innerRange = this.adjustRange(pairInfo.innerRange)
+ pairInfo.targetRange = this.isInner() ? pairInfo.innerRange : pairInfo.aRange
+ return pairInfo
+ }
+ }
+
+ getRange(selection) {
+ const originalRange = selection.getBufferRange()
+ let pairInfo = this.getPairInfo(this.getCursorPositionForSelection(selection))
+ // When range was same, try to expand range
+ if (pairInfo && pairInfo.targetRange.isEqual(originalRange)) {
+ pairInfo = this.getPairInfo(pairInfo.aRange.end)
+ }
+ if (pairInfo) return pairInfo.targetRange
+ }
+}
+Pair.register(false)
+
+// Used by DeleteSurround
+class APair extends Pair {}
+APair.register(false)
+
+class AnyPair extends Pair {
+ allowForwarding = false
+ member = ["DoubleQuote", "SingleQuote", "BackTick", "CurlyBracket", "AngleBracket", "SquareBracket", "Parenthesis"]
+
+ getRanges(selection) {
+ const options = {inner: this.inner, allowForwarding: this.allowForwarding, inclusive: this.inclusive}
+ return this.member.map(member => this.getInstance(member, options).getRange(selection)).filter(range => range)
+ }
+
+ getRange(selection) {
+ return _.last(sortRanges(this.getRanges(selection)))
+ }
+}
+AnyPair.register(false, true)
+
+class AnyPairAllowForwarding extends AnyPair {
+ allowForwarding = true
+
+ getRange(selection) {
+ const ranges = this.getRanges(selection)
+ const from = selection.cursor.getBufferPosition()
+ let [forwardingRanges, enclosingRanges] = _.partition(ranges, range => range.start.isGreaterThanOrEqual(from))
+ const enclosingRange = _.last(sortRanges(enclosingRanges))
+ forwardingRanges = sortRanges(forwardingRanges)
+
+ // When enclosingRange is exists,
+ // We don't go across enclosingRange.end.
+ // So choose from ranges contained in enclosingRange.
+ if (enclosingRange) {
+ forwardingRanges = forwardingRanges.filter(range => enclosingRange.containsRange(range))
+ }
+
+ return forwardingRanges[0] || enclosingRange
+ }
+}
+AnyPairAllowForwarding.register(false, true)
+
+class AnyQuote extends AnyPair {
+ allowForwarding = true
+ member = ["DoubleQuote", "SingleQuote", "BackTick"]
+
+ getRange(selection) {
+ const ranges = this.getRanges(selection)
+ // Pick range which end.colum is leftmost(mean, closed first)
+ if (ranges.length) return _.first(_.sortBy(ranges, r => r.end.column))
+ }
+}
+AnyQuote.register(false, true)
+
+class Quote extends Pair {
+ allowForwarding = true
+}
+Quote.register(false)
+
+class DoubleQuote extends Quote {
+ pair = ['"', '"']
+}
+DoubleQuote.register(false, true)
+
+class SingleQuote extends Quote {
+ pair = ["'", "'"]
+}
+SingleQuote.register(false, true)
+
+class BackTick extends Quote {
+ pair = ["`", "`"]
+}
+BackTick.register(false, true)
+
+class CurlyBracket extends Pair {
+ pair = ["{", "}"]
+}
+CurlyBracket.register(false, true, true)
+
+class SquareBracket extends Pair {
+ pair = ["[", "]"]
+}
+SquareBracket.register(false, true, true)
+
+class Parenthesis extends Pair {
+ pair = ["(", ")"]
+}
+Parenthesis.register(false, true, true)
+
+class AngleBracket extends Pair {
+ pair = ["<", ">"]
+}
+AngleBracket.register(false, true, true)
+
+class Tag extends Pair {
+ allowNextLine = true
+ allowForwarding = true
+ adjustInnerRange = false
+
+ getTagStartPoint(from) {
+ let tagRange
+ const {pattern} = PairFinder.TagFinder
+ this.scanForward(pattern, {from: [from.row, 0]}, ({range, stop}) => {
+ if (range.containsPoint(from, true)) {
+ tagRange = range
+ stop()
+ }
+ })
+ if (tagRange) return tagRange.start
+ }
+
+ getFinder() {
+ return new PairFinder.TagFinder(this.editor, {
+ allowNextLine: this.isAllowNextLine(),
+ allowForwarding: this.allowForwarding,
+ inclusive: this.inclusive,
+ })
+ }
+
+ getPairInfo(from) {
+ return super.getPairInfo(this.getTagStartPoint(from) || from)
+ }
+}
+Tag.register(false, true)
+
+// Section: Paragraph
+// =========================
+// Paragraph is defined as consecutive (non-)blank-line.
+class Paragraph extends TextObject {
+ wise = "linewise"
+ supportCount = true
+
+ findRow(fromRow, direction, fn) {
+ if (fn.reset) fn.reset()
+ let foundRow = fromRow
+ for (const row of getBufferRows(this.editor, {startRow: fromRow, direction})) {
+ if (!fn(row, direction)) break
+ foundRow = row
+ }
+ return foundRow
+ }
+
+ findRowRangeBy(fromRow, fn) {
+ const startRow = this.findRow(fromRow, "previous", fn)
+ const endRow = this.findRow(fromRow, "next", fn)
+ return [startRow, endRow]
+ }
+
+ getPredictFunction(fromRow, selection) {
+ const fromRowResult = this.editor.isBufferRowBlank(fromRow)
+
+ if (this.isInner()) {
+ return (row, direction) => this.editor.isBufferRowBlank(row) === fromRowResult
+ } else {
+ const directionToExtend = selection.isReversed() ? "previous" : "next"
+
+ let flip = false
+ const predict = (row, direction) => {
+ const result = this.editor.isBufferRowBlank(row) === fromRowResult
+ if (flip) {
+ return !result
+ } else {
+ if (!result && direction === directionToExtend) {
+ return (flip = true)
+ }
+ return result
+ }
+ }
+ predict.reset = () => (flip = false)
+ return predict
+ }
+ }
+
+ getRange(selection) {
+ const originalRange = selection.getBufferRange()
+ let fromRow = this.getCursorPositionForSelection(selection).row
+ if (this.isMode("visual", "linewise")) {
+ if (selection.isReversed()) fromRow--
+ else fromRow++
+ fromRow = getValidVimBufferRow(this.editor, fromRow)
+ }
+ const rowRange = this.findRowRangeBy(fromRow, this.getPredictFunction(fromRow, selection))
+ return selection.getBufferRange().union(this.getBufferRangeForRowRange(rowRange))
+ }
+}
+Paragraph.register(false, true)
+
+class Indentation extends Paragraph {
+ getRange(selection) {
+ const fromRow = this.getCursorPositionForSelection(selection).row
+ const baseIndentLevel = this.editor.indentationForBufferRow(fromRow)
+ const rowRange = this.findRowRangeBy(fromRow, row => {
+ return this.editor.isBufferRowBlank(row)
+ ? this.isA()
+ : this.editor.indentationForBufferRow(row) >= baseIndentLevel
+ })
+ return this.getBufferRangeForRowRange(rowRange)
+ }
+}
+Indentation.register(false, true)
+
+// Section: Comment
+// =========================
+class Comment extends TextObject {
+ wise = "linewise"
+
+ getRange(selection) {
+ const {row} = this.getCursorPositionForSelection(selection)
+ const rowRange = this.utils.getRowRangeForCommentAtBufferRow(this.editor, row)
+ if (rowRange) {
+ return this.getBufferRangeForRowRange(rowRange)
+ }
+ }
+}
+Comment.register(false, true)
+
+class CommentOrParagraph extends TextObject {
+ wise = "linewise"
+
+ getRange(selection) {
+ const {inner} = this
+ for (const klass of ["Comment", "Paragraph"]) {
+ const range = this.getInstance(klass, {inner}).getRange(selection)
+ if (range) return range
+ }
+ }
+}
+CommentOrParagraph.register(false, true)
+
+// Section: Fold
+// =========================
+class Fold extends TextObject {
+ wise = "linewise"
+
+ adjustRowRange(rowRange) {
+ if (this.isA()) return rowRange
+
+ let [startRow, endRow] = rowRange
+ if (this.editor.indentationForBufferRow(startRow) === this.editor.indentationForBufferRow(endRow)) {
+ endRow -= 1
+ }
+ startRow += 1
+ return [startRow, endRow]
+ }
+
+ getFoldRowRangesContainsForRow(row) {
+ return getCodeFoldRowRangesContainesForRow(this.editor, row).reverse()
+ }
+
+ getRange(selection) {
+ const {row} = this.getCursorPositionForSelection(selection)
+ const selectedRange = selection.getBufferRange()
+ for (const rowRange of this.getFoldRowRangesContainsForRow(row)) {
+ const range = this.getBufferRangeForRowRange(this.adjustRowRange(rowRange))
+
+ // Don't change to `if range.containsRange(selectedRange, true)`
+ // There is behavior diff when cursor is at beginning of line( column 0 ).
+ if (!selectedRange.containsRange(range)) return range
+ }
+ }
+}
+Fold.register(false, true)
+
+// NOTE: Function range determination is depending on fold.
+class Function extends Fold {
+ // Some language don't include closing `}` into fold.
+ scopeNamesOmittingEndRow = ["source.go", "source.elixir"]
+
+ isGrammarNotFoldEndRow() {
+ const {scopeName, packageName} = this.editor.getGrammar()
+ if (this.scopeNamesOmittingEndRow.includes(scopeName)) {
+ return true
+ } else {
+ // HACK: Rust have two package `language-rust` and `atom-language-rust`
+ // language-rust don't fold ending `}`, but atom-language-rust does.
+ return scopeName === "source.rust" && packageName === "language-rust"
+ }
+ }
+
+ getFoldRowRangesContainsForRow(row) {
+ return super.getFoldRowRangesContainsForRow(row).filter(rowRange => {
+ return isIncludeFunctionScopeForRow(this.editor, rowRange[0])
+ })
+ }
+
+ adjustRowRange(rowRange) {
+ let [startRow, endRow] = super.adjustRowRange(rowRange)
+ // NOTE: This adjustment shoud not be necessary if language-syntax is properly defined.
+ if (this.isA() && this.isGrammarNotFoldEndRow()) endRow += 1
+ return [startRow, endRow]
+ }
+}
+Function.register(false, true)
+
+// Section: Other
+// =========================
+class Arguments extends TextObject {
+ newArgInfo(argStart, arg, separator) {
+ const argEnd = traverseTextFromPoint(argStart, arg)
+ const argRange = new Range(argStart, argEnd)
+
+ const separatorEnd = traverseTextFromPoint(argEnd, separator != null ? separator : "")
+ const separatorRange = new Range(argEnd, separatorEnd)
+
+ const innerRange = argRange
+ const aRange = argRange.union(separatorRange)
+ return {argRange, separatorRange, innerRange, aRange}
+ }
+
+ getArgumentsRangeForSelection(selection) {
+ const options = {
+ member: ["CurlyBracket", "SquareBracket", "Parenthesis"],
+ inclusive: false,
+ }
+ return this.getInstance("InnerAnyPair", options).getRange(selection)
+ }
+
+ getRange(selection) {
+ let range = this.getArgumentsRangeForSelection(selection)
+ const pairRangeFound = range != null
+
+ range = range || this.getInstance("InnerCurrentLine").getRange(selection) // fallback
+ if (!range) return
+
+ range = trimRange(this.editor, range)
+
+ const text = this.editor.getTextInBufferRange(range)
+ const allTokens = splitArguments(text, pairRangeFound)
+
+ const argInfos = []
+ let argStart = range.start
+
+ // Skip starting separator
+ if (allTokens.length && allTokens[0].type === "separator") {
+ const token = allTokens.shift()
+ argStart = traverseTextFromPoint(argStart, token.text)
+ }
+
+ while (allTokens.length) {
+ const token = allTokens.shift()
+ if (token.type === "argument") {
+ const nextToken = allTokens.shift()
+ const separator = nextToken ? nextToken.text : undefined
+ const argInfo = this.newArgInfo(argStart, token.text, separator)
+
+ if (allTokens.length === 0 && argInfos.length) {
+ argInfo.aRange = argInfo.argRange.union(_.last(argInfos).separatorRange)
+ }
+
+ argStart = argInfo.aRange.end
+ argInfos.push(argInfo)
+ } else {
+ throw new Error("must not happen")
+ }
+ }
+
+ const point = this.getCursorPositionForSelection(selection)
+ for (const {innerRange, aRange} of argInfos) {
+ if (innerRange.end.isGreaterThanOrEqual(point)) {
+ return this.isInner() ? innerRange : aRange
+ }
+ }
+ }
+}
+Arguments.register(false, true)
+
+class CurrentLine extends TextObject {
+ getRange(selection) {
+ const {row} = this.getCursorPositionForSelection(selection)
+ const range = this.editor.bufferRangeForBufferRow(row)
+ return this.isA() ? range : trimRange(this.editor, range)
+ }
+}
+CurrentLine.register(false, true)
+
+class Entire extends TextObject {
+ wise = "linewise"
+ selectOnce = true
+
+ getRange(selection) {
+ return this.editor.buffer.getRange()
+ }
+}
+Entire.register(false, true)
+
+class Empty extends TextObject {
+ selectOnce = true
+}
+Empty.register(false)
+
+class LatestChange extends TextObject {
+ wise = null
+ selectOnce = true
+ getRange(selection) {
+ const start = this.vimState.mark.get("[")
+ const end = this.vimState.mark.get("]")
+ if (start && end) {
+ return new Range(start, end)
+ }
+ }
+}
+LatestChange.register(false, true)
+
+class SearchMatchForward extends TextObject {
+ backward = false
+
+ findMatch(fromPoint, pattern) {
+ if (this.mode === "visual") {
+ fromPoint = translatePointAndClip(this.editor, fromPoint, "forward")
+ }
+ let foundRange
+ this.scanForward(pattern, {from: [fromPoint.row, 0]}, ({range, stop}) => {
+ if (range.end.isGreaterThan(fromPoint)) {
+ foundRange = range
+ stop()
+ }
+ })
+ return {range: foundRange, whichIsHead: "end"}
+ }
+
+ getRange(selection) {
+ const pattern = this.globalState.get("lastSearchPattern")
+ if (!pattern) return
+
+ const fromPoint = selection.getHeadBufferPosition()
+ const {range, whichIsHead} = this.findMatch(fromPoint, pattern)
+ if (range) {
+ return this.unionRangeAndDetermineReversedState(selection, range, whichIsHead)
+ }
+ }
+
+ unionRangeAndDetermineReversedState(selection, range, whichIsHead) {
+ if (selection.isEmpty()) return range
+
+ let head = range[whichIsHead]
+ const tail = selection.getTailBufferPosition()
+
+ if (this.backward) {
+ if (tail.isLessThan(head)) head = translatePointAndClip(this.editor, head, "forward")
+ } else {
+ if (head.isLessThan(tail)) head = translatePointAndClip(this.editor, head, "backward")
+ }
+
+ this.reversed = head.isLessThan(tail)
+ return new Range(tail, head).union(this.swrap(selection).getTailBufferRange())
+ }
+
+ selectTextObject(selection) {
+ const range = this.getRange(selection)
+ if (range) {
+ this.swrap(selection).setBufferRange(range, {reversed: this.reversed != null ? this.reversed : this.backward})
+ return true
+ }
+ }
+}
+SearchMatchForward.register()
+
+class SearchMatchBackward extends SearchMatchForward {
+ backward = true
+
+ findMatch(fromPoint, pattern) {
+ if (this.mode === "visual") {
+ fromPoint = translatePointAndClip(this.editor, fromPoint, "backward")
+ }
+ let foundRange
+ this.scanBackward(pattern, {from: [fromPoint.row, Infinity]}, ({range, stop}) => {
+ if (range.start.isLessThan(fromPoint)) {
+ foundRange = range
+ stop()
+ }
+ })
+ return {range: foundRange, whichIsHead: "start"}
+ }
+}
+SearchMatchBackward.register()
+
+// [Limitation: won't fix]: Selected range is not submode aware. always characterwise.
+// So even if original selection was vL or vB, selected range by this text-object
+// is always vC range.
+class PreviousSelection extends TextObject {
+ wise = null
+ selectOnce = true
+
+ selectTextObject(selection) {
+ const {properties, submode} = this.vimState.previousSelection
+ if (properties && submode) {
+ this.wise = submode
+ this.swrap(this.editor.getLastSelection()).selectByProperties(properties)
+ return true
+ }
+ }
+}
+PreviousSelection.register()
+
+class PersistentSelection extends TextObject {
+ wise = null
+ selectOnce = true
+
+ selectTextObject(selection) {
+ if (this.vimState.hasPersistentSelections()) {
+ this.persistentSelection.setSelectedBufferRanges()
+ return true
+ }
+ }
+}
+PersistentSelection.register(false, true)
+
+// Used only by ReplaceWithRegister and PutBefore and its' children.
+class LastPastedRange extends TextObject {
+ wise = null
+ selectOnce = true
+
+ selectTextObject(selection) {
+ for (selection of this.editor.getSelections()) {
+ const range = this.vimState.sequentialPasteManager.getPastedRangeForSelection(selection)
+ selection.setBufferRange(range)
+ }
+ return true
+ }
+}
+LastPastedRange.register(false)
+
+class VisibleArea extends TextObject {
+ selectOnce = true
+
+ getRange(selection) {
+ // [BUG?] Need translate to shilnk top and bottom to fit actual row.
+ // The reason I need -2 at bottom is because of status bar?
+ const range = getVisibleBufferRange(this.editor)
+ return range.getRows() > this.editor.getRowsPerPage() ? range.translate([+1, 0], [-3, 0]) : range
+ }
+}
+VisibleArea.register(false, true)
diff --git a/lib/utils.js b/lib/utils.js
index 8d5384772..3b4a0597d 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,4 +1,4 @@
-let fs
+let fs, semver
const settings = require("./settings")
const {Range, Point} = require("atom")
const _ = require("underscore-plus")
@@ -177,10 +177,10 @@ function moveCursorToNextNonWhitespace(cursor) {
function getBufferRows(editor, {startRow, direction}) {
switch (direction) {
case "previous":
- return startRow <= 0 ? [] : getRange(startRow - 1, 0)
+ return startRow <= 0 ? [] : getList(startRow - 1, 0)
case "next":
const endRow = getVimLastBufferRow(editor)
- return startRow >= endRow ? [] : getRange(startRow + 1, endRow)
+ return startRow >= endRow ? [] : getList(startRow + 1, endRow)
}
}
@@ -343,14 +343,19 @@ function getLineTextToBufferPosition(editor, {row, column}, {exclusive = true} =
return editor.lineTextForBufferRow(row).slice(0, exclusive ? column : column + 1)
}
-function getIndentLevelForBufferRow(editor, row) {
- return editor.indentLevelForLine(editor.lineTextForBufferRow(row))
-}
-
function getCodeFoldRowRanges(editor) {
- return getRange(0, editor.getLastBufferRow())
- .map(row => editor.languageMode.rowRangeForCodeFoldAtBufferRow(row))
- .filter(rowRange => rowRange != null && rowRange[0] != null && rowRange[1] != null)
+ if (atomVersionSatisfies(">=1.22.0-beta0")) {
+ return editor.tokenizedBuffer
+ .getFoldableRanges()
+ .filter(range => !editor.tokenizedBuffer.isRowCommented(range.start.row))
+ .map(range => [range.start.row, range.end.row])
+ } else {
+ const seen = {}
+ return getList(0, editor.getLastBufferRow())
+ .map(row => editor.languageMode.rowRangeForCodeFoldAtBufferRow(row))
+ .filter(rowRange => rowRange != null && rowRange[0] != null && rowRange[1] != null)
+ .filter(rowRange => (seen[rowRange] ? false : (seen[rowRange] = true)))
+ }
}
// Used in vmp-jasmine-increase-focus
@@ -365,25 +370,13 @@ function getCodeFoldRowRangesContainesForRow(editor, bufferRow, {includeStartRow
function getFoldRowRangesContainedByFoldStartsAtRow(editor, row) {
if (!editor.isFoldableAtBufferRow(row)) return null
- const [startRow, endRow] = editor.languageMode.rowRangeForFoldAtBufferRow(row)
-
- const seen = {}
- return getRange(startRow, endRow)
- .map(row => editor.languageMode.rowRangeForFoldAtBufferRow(row))
- .filter(rowRange => rowRange != null && rowRange[0] != null && rowRange[1] != null)
- .filter(rowRange => (seen[rowRange] ? false : (seen[rowRange] = true)))
-}
-
-function getFoldRowRanges(editor) {
- const seen = {}
- return getRange(0, editor.getLastBufferRow())
- .map(row => editor.languageMode.rowRangeForCodeFoldAtBufferRow(row))
- .filter(rowRange => rowRange != null && rowRange[0] != null && rowRange[1] != null)
- .filter(rowRange => (seen[rowRange] ? false : (seen[rowRange] = true)))
+ const rowRanges = getCodeFoldRowRanges(editor)
+ const foldRowRange = rowRanges.find(rowRange => rowRange[0] === row)
+ return rowRanges.filter(rowRange => foldRowRange[0] <= rowRange[0] && foldRowRange[1] >= rowRange[1])
}
function getFoldRangesWithIndent(editor) {
- return getFoldRowRanges(editor).map(([startRow, endRow]) => ({
+ return getCodeFoldRowRanges(editor).map(([startRow, endRow]) => ({
startRow,
endRow,
indent: editor.indentationForBufferRow(startRow),
@@ -417,9 +410,7 @@ function getFoldInfoByKind(editor) {
}
function getBufferRangeForRowRange(editor, [startRow, endRow]) {
- const startRange = editor.bufferRangeForBufferRow(startRow, {includeNewline: true})
- const endRange = editor.bufferRangeForBufferRow(endRow, {includeNewline: true})
- return startRange.union(endRange)
+ return new Range([startRow, 0], [startRow, 0]).union(editor.bufferRangeForBufferRow(endRow, {includeNewline: true}))
}
function getTokenizedLineForRow(editor, row) {
@@ -435,10 +426,10 @@ function scanForScopeStart(editor, fromPoint, direction, fn) {
let scanRows, isValidToken
if (direction === "forward") {
- scanRows = getRange(fromPoint.row, editor.getLastBufferRow())
+ scanRows = getList(fromPoint.row, editor.getLastBufferRow())
isValidToken = ({position}) => position.isGreaterThan(fromPoint)
} else if (direction === "backward") {
- scanRows = getRange(fromPoint.row, 0)
+ scanRows = getList(fromPoint.row, 0)
isValidToken = ({position}) => position.isLessThan(fromPoint)
}
@@ -881,6 +872,9 @@ function splitAndJoinBy(text, regex, fn) {
return leadingSpaces + newText + trailingSpaces
}
+// Return list of argument token.
+// Token is object like {text: String, type: String}
+// type should be "separator" or "argument"
function splitArguments(text, joinSpaceSeparatedToken = true) {
const separatorChars = "\t, \r\n"
const quoteChars = "\"'`"
@@ -1015,9 +1009,9 @@ function adjustIndentWithKeepingLayout(editor, range) {
const rowAndActualLevels = []
let minLevel
- for (const row of getRange(range.start.row, range.end.row, false)) {
+ for (const row of getList(range.start.row, range.end.row, false)) {
if (isEmptyRow(editor, row)) continue
- const actualLevel = getIndentLevelForBufferRow(editor, row)
+ const actualLevel = editor.indentationForBufferRow(row)
rowAndActualLevels.push([row, actualLevel])
minLevel = minLevel == null ? actualLevel : Math.min(minLevel, actualLevel)
}
@@ -1054,12 +1048,17 @@ function getTraversalForText(text) {
return new Point(row, column)
}
+// Return startRow of fold if row was folded or just return passed row.
+function getFoldStartRowForRow(editor, row) {
+ return editor.isFoldedAtBufferRow(row) ? getLargestFoldRangeContainsBufferRow(editor, row).start.row : row
+}
+
// Return endRow of fold if row was folded or just return passed row.
function getFoldEndRowForRow(editor, row) {
return editor.isFoldedAtBufferRow(row) ? getLargestFoldRangeContainsBufferRow(editor, row).end.row : row
}
-function getRange(start, end, inclusive = true) {
+function getList(start, end, inclusive = true) {
const range = []
if (start < end) {
if (inclusive) for (let i = start; i <= end; i++) range.push(i)
@@ -1118,6 +1117,7 @@ function detectMinimumIndentLengthInText(text) {
return minIndent === Infinity ? 0 : minIndent
}
+// FIXME: really, this is garbage.
function normalizeIndent(text, editor, targetRange) {
// text = convertTabToSpace(text, editor.getTabLength())
const mapEachLine = (text, fn) =>
@@ -1143,6 +1143,29 @@ function normalizeIndent(text, editor, targetRange) {
return text
}
+function atomVersionSatisfies(condition) {
+ if (!semver) semver = require("semver")
+ return semver.satisfies(atom.appVersion, condition)
+}
+
+function getRowRangeForCommentAtBufferRow(editor, row) {
+ if (atomVersionSatisfies(">=1.22.0-beta0")) {
+ isRowCommented = row => editor.tokenizedBuffer.isRowCommented(row)
+ if (!isRowCommented(row)) return
+
+ let startRow = row
+ let endRow = row
+
+ while (isRowCommented(startRow - 1)) startRow--
+ while (isRowCommented(endRow + 1)) endRow++
+
+ return [startRow, endRow]
+ } else {
+ if (!editor.isBufferRowCommented(row)) return
+ return editor.languageMode.rowRangeForCommentAtBufferRow(row) || [row, row]
+ }
+}
+
module.exports = {
assertWithException,
getAncestors,
@@ -1176,14 +1199,12 @@ module.exports = {
getValidVimScreenRow,
moveCursorToFirstCharacterAtRow,
getLineTextToBufferPosition,
- getIndentLevelForBufferRow,
getTextInScreenRange,
moveCursorToNextNonWhitespace,
isEmptyRow,
getCodeFoldRowRanges,
getCodeFoldRowRangesContainesForRow,
getFoldRowRangesContainedByFoldStartsAtRow,
- getFoldRowRanges,
getFoldRangesWithIndent,
getFoldInfoByKind,
getBufferRangeForRowRange,
@@ -1234,9 +1255,12 @@ module.exports = {
adjustIndentWithKeepingLayout,
rangeContainsPointWithEndExclusive,
traverseTextFromPoint,
+ getFoldStartRowForRow,
getFoldEndRowForRow,
- getRange,
+ getList,
unindent,
removeIndent,
normalizeIndent,
+ atomVersionSatisfies,
+ getRowRangeForCommentAtBufferRow,
}
diff --git a/lib/vim-state.js b/lib/vim-state.js
index 40142e210..3b17f5d2d 100644
--- a/lib/vim-state.js
+++ b/lib/vim-state.js
@@ -1,4 +1,3 @@
-const Delegato = require("delegato")
let jQuery, focusInput
const {Emitter, Disposable, CompositeDisposable, Range} = require("atom")
@@ -73,6 +72,9 @@ module.exports = class VimState {
getCount(...args) {
return this.operationStack.getCount(...args)
}
+ hasCount(...args) {
+ return this.operationStack.hasCount(...args)
+ }
setCount(...args) {
this.operationStack.setCount(...args)
}
diff --git a/package.json b/package.json
index 04fbcdfcf..2d5bec5a0 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,9 @@
},
"dependencies": {
"atom-space-pen-views": "^2.1.1",
- "delegato": "^1.0.0",
"fs-plus": "^2.8.1",
"fuzzaldrin": "^2.1.0",
"season": "^6.0.0",
- "semver": "^5.1.0",
"underscore-plus": "1.x"
},
"consumedServices": {
@@ -41,7 +39,7 @@
"devDependencies": {
"coffeelint": "^1.10.1",
"prettier": "^1.7.0",
- "semver": "^5.3.0"
+ "semver": "^5.4.1"
},
"scripts": {
"test": "apm test",
diff --git a/spec/fast-activation-spec.coffee b/spec/fast-activation-spec.coffee
index 83814c1c3..bfe5a2d9c 100644
--- a/spec/fast-activation-spec.coffee
+++ b/spec/fast-activation-spec.coffee
@@ -62,9 +62,7 @@ describe "dirty work for fast package activation", ->
describe "requrie as minimum num of file as possible on startup", ->
shouldRequireFilesInOrdered = [
"lib/main.js"
- "lib/base.coffee"
- "node_modules/delegato/lib/delegator.js"
- "node_modules/mixto/lib/mixin.js"
+ "lib/base.js"
"lib/settings.js"
"lib/global-state.js"
"lib/vim-state.js"
@@ -110,7 +108,7 @@ describe "dirty work for fast package activation", ->
"node_modules/underscore-plus/lib/underscore-plus.js"
"node_modules/underscore/underscore.js"
"lib/blockwise-selection.js"
- "lib/motion.coffee"
+ "lib/motion.js"
"lib/cursor-style-manager.js"
]
files = shouldRequireFilesInOrdered.concat(extraShouldRequireFilesInOrdered)
diff --git a/spec/motion-find-spec.coffee b/spec/motion-find-spec.coffee
index 1e5a8f924..85b48b0de 100644
--- a/spec/motion-find-spec.coffee
+++ b/spec/motion-find-spec.coffee
@@ -371,7 +371,7 @@ describe "Motion Find", ->
describe "can find one or two char", ->
it "adjust to next-pre-confirmed", ->
set textC: "| a ab a cd a"
- keystroke "f a "
+ keystroke "f a"
element = vimState.inputEditor.element
dispatch(element, "vim-mode-plus:find-next-pre-confirmed")
dispatch(element, "vim-mode-plus:find-next-pre-confirmed")
diff --git a/spec/text-object-spec.coffee b/spec/text-object-spec.coffee
index 58bb029af..d157ad716 100644
--- a/spec/text-object-spec.coffee
+++ b/spec/text-object-spec.coffee
@@ -1747,7 +1747,7 @@ describe "TextObject", ->
}
"""
- describe 'slingle line comma separated text', ->
+ describe 'single line comma separated text', ->
describe "change 1st arg", ->
beforeEach -> set textC: "var a = func(f|irst(1, 2, 3), second(), 3)"
it 'change', -> ensure 'c i ,', textC: "var a = func(|, second(), 3)"