helpers.coffee | |
---|---|
Define the base class for all commands. | class exports.Command
2 | constructor: (@count = 1) ->
3 | isRepeatable: yes |
If the class specifies a regex for char(s) that should follow the command, 4 | then the command isn't complete until those char(s) have been matched. | isComplete: ->
5 | if @constructor.followedBy then @followedBy else true |
A bunch of commands can just repeat an action however many times their | exports.repeatCountTimes = (func) ->
8 | (jim) ->
9 | timesLeft = @count
10 | func.call this, jim while timesLeft--
11 |
12 | |
jim.coffee | |
---|---|
An instance of | Keymap = require './keymap'
6 | {GoToLine} = require './motions'
7 |
8 | class Jim
9 | @VERSION: '0.2.0-pre'
10 |
11 | constructor: (@adaptor) ->
12 | @command = null
13 | @registers = {}
14 | @keymap = Keymap.getDefault()
15 | @setMode 'normal'
16 |
17 | modes: require './modes' |
Change Jim's mode to | setMode: (modeName, modeState) ->
21 | console.log 'setMode', modeName, modeState if @debugMode
22 | prevMode = @mode
23 | if modeName is prevMode?.name
24 | return unless modeState
25 | @mode[key] = value for own key, value of modeState
26 | else
27 | @mode = modeState or {}
28 | @mode.name = modeName
29 |
30 | @adaptor.onModeChange? prevMode, @mode
31 |
32 | switch prevMode?.name
33 | when 'insert'
34 | @adaptor.moveLeft() |
Get info about what was inserted so the insert "remembers" how to 35 | repeat itself. | @lastCommand.repeatableInsert = @adaptor.lastInsert()
36 |
37 | when 'replace'
38 | @adaptor.setOverwriteMode off |
Pressing escape blows away all the state. | onEscape: ->
39 | @setMode 'normal'
40 | @command = null
41 | @commandPart = '' # just in case...
42 | @adaptor.clearSelection() |
When a key is pressed, let the current mode figure out what to do about it. | onKeypress: (keys) -> @modes[@mode.name].onKeypress.call this, keys |
Delete the selected text and put it in the default register. | deleteSelection: (exclusive, linewise) ->
43 | @registers['"'] = @adaptor.deleteSelection exclusive, linewise
44 | |
Yank the selected text into the default register. | yankSelection: (exclusive, linewise) ->
45 | @registers['"'] = @adaptor.selectionText exclusive, linewise
46 | @adaptor.clearSelection true
47 |
48 | module.exports = Jim
49 |
50 | |
keymap.coffee | |
---|---|
This is a pretty standard key-to-command keymap except for a few details: 2 | 3 |
| class Keymap |
Building a Keymap | |
Build an instance of | @getDefault: ->
8 | keymap = new Keymap
9 | keymap.mapCommand keys, commandClass for own keys, commandClass of require('./commands').defaultMappings
10 | keymap.mapOperator keys, operationClass for own keys, operationClass of require('./operators').defaultMappings
11 | keymap.mapMotion keys, motionClass for own keys, motionClass of require('./motions').defaultMappings
12 | keymap
13 |
14 | constructor: ->
15 | @commands = {}
16 | @motions = {}
17 | @visualCommands = {} |
Use some objects to de-duplicate repeated partial commands. | @partialCommands = {}
18 | @partialMotions = {}
19 | @partialVisualCommands = {} |
Mapping commands | |
Map the | mapCommand: (keys, commandClass) ->
21 | if commandClass::exec
22 | @commands[keys] = commandClass
23 | if keys.length is 2
24 | @partialCommands[keys[0]] = true
25 | if commandClass::visualExec
26 | @visualCommands[keys] = commandClass
27 | if keys.length is 2
28 | @partialVisualCommands[keys[0]] = true |
Map | mapMotion: (keys, motionClass) ->
29 | @commands[keys] = motionClass
30 | @motions[keys] = motionClass
31 | @visualCommands[keys] = motionClass
32 | if keys.length is 2
33 | @partialMotions[keys[0]] = true
34 | @partialCommands[keys[0]] = true
35 | @partialVisualCommands[keys[0]] = true |
Map | mapOperator: (keys, operatorClass) ->
36 | @commands[keys] = operatorClass
37 | @visualCommands[keys] = operatorClass
38 | if keys.length is 2
39 | @partialCommands[keys[0]] = true
40 | @partialVisualCommands[keys[0]] = true |
Finding commands in the Keymap41 | 42 |
| |
Build a regex that will help us split up the
| buildPartialCommandRegex = (partialCommands) ->
66 | ///
67 | ^
68 | ([1-9]\d*)?
69 | (
70 | [#{(char for own char, nothing of partialCommands).join ''}]?
71 | ([\s\S]*)
72 | )?
73 | $
74 | /// |
Find a normal mode command, which could be a motion, an operator, or a 75 | "regular" normal mode command. | commandFor: (commandPart) ->
76 | @partialCommandRegex or= buildPartialCommandRegex @partialCommands
77 | [commandPart, count, command, beyondPartial] = commandPart.match @partialCommandRegex
78 |
79 | if beyondPartial
80 | if commandClass = @commands[command]
81 | new commandClass(parseInt(count) or null)
82 | else
83 | false
84 | else
85 | true |
Find a motion. | motionFor: (commandPart, operatorPending) ->
86 | @partialMotionRegex or= buildPartialCommandRegex @partialMotions
87 | [commandPart, count, motion, beyondPartial] = commandPart.match @partialCommandRegex
88 |
89 | if beyondPartial
90 | if motion is operatorPending |
If we're finding | {LinewiseCommandMotion} = require './motions'
91 | new LinewiseCommandMotion(parseInt(count) or null)
92 |
93 | else if motionClass = @motions[motion]
94 | new motionClass(parseInt(count) or null)
95 | else
96 | false
97 | else
98 | true |
Find a visual mode command, which could be a motion, an operator, or a 99 | "regular" visual mode command. | visualCommandFor: (commandPart) ->
100 | @partialVisualCommandRegex or= buildPartialCommandRegex @partialVisualCommands
101 | [commandPart, count, command, beyondPartial] = commandPart.match @partialVisualCommandRegex
102 |
103 | if beyondPartial
104 | if commandClass = @visualCommands[command]
105 | new commandClass(parseInt(count) or null)
106 | else
107 | false
108 | else
109 | true |
Exports | module.exports = Keymap
110 |
111 | |
modes.coffee | |
---|---|
Each mode handles key presses a bit differently. For instance, typing an 2 | operator in visual mode immediately operates on the selected text. In normal 3 | mode Jim waits for a motion to follow the operator. All of the modes' 4 | keyboard handling is defined here. 5 | 6 |Each mode's
| {MoveLeft, MoveDown} = require './motions' |
Shame the user in the console for not knowing their Jim commands. | invalidCommand = (type = 'command') ->
18 | console.log "invalid #{type}: #{@commandPart}"
19 | @onEscape() |
Normal mode (a.k.a. "command mode") | exports.normal =
20 | onKeypress: (keys) ->
21 | @commandPart = (@commandPart or '') + keys
22 |
23 | if not @command
24 | command = @keymap.commandFor @commandPart
25 |
26 | if command is false
27 | invalidCommand.call this
28 | else if command isnt true
29 | if command.isOperation |
Hang onto the pending operator so that double-operators can
30 | recognized ( | [@operatorPending] = @commandPart.match /[^\d]+$/
31 |
32 | @command = command
33 | @commandPart = ''
34 | else if @command.constructor.followedBy |
If we've got a command that expects a key to follow it, check if
35 | | if @command.constructor.followedBy.test @commandPart
36 | @command.followedBy = @commandPart
37 | else
38 | console.log "#{@command} didn't expect to be followed by \"#{@commandPart}\""
39 |
40 | @commandPart = ''
41 | else if @command.isOperation
42 | if regex = @command.motion?.constructor.followedBy |
If we've got a motion that expects a key to follow it, check if
43 | | if regex.test @commandPart
44 | @command.motion.followedBy = @commandPart
45 | else
46 | console.log "#{@command} didn't expect to be followed by \"#{@commandPart}\""
47 |
48 | else
49 | motion = @keymap.motionFor @commandPart, @operatorPending
50 |
51 | if motion is false
52 | invalidCommand.call this, 'motion'
53 | else if motion isnt true |
Motions need a reference to the operation they're a part of since it
54 | sometimes changes the amount of text they move over (e.g. | motion.operation = @command
56 |
57 | @command.motion = motion
58 | @operatorPending = null
59 | @commandPart = '' |
Execute the command if it's complete, otherwise wait for more keys. | if @command?.isComplete()
60 | @command.exec this
61 | @lastCommand = @command if @command.isRepeatable
62 | @command = null |
Visual mode | exports.visual =
63 | onKeypress: (newKeys) ->
64 | @commandPart = (@commandPart or '') + newKeys
65 |
66 | if not @command
67 | command = @keymap.visualCommandFor @commandPart
68 |
69 | if command is false
70 | invalidCommand.call this
71 | else if command isnt true
72 | @command = command
73 | @commandPart = ''
74 | else if @command.constructor.followedBy |
If we've got a motion that expects a key to follow it, check if
75 | | if @command.constructor.followedBy.test @commandPart
76 | @command.followedBy = @commandPart
77 | else
78 | console.log "#{@command} didn't expect to be followed by \"#{@commandPart}\""
79 | @commandPart = ''
80 |
81 | wasBackwards = @adaptor.isSelectionBackwards() |
Operations are always "complete" in visual mode. | if @command?.isOperation or @command?.isComplete()
82 | if @command.isRepeatable |
Save the selection's "size", which will be used if the command is 83 | repeated. | @command.selectionSize = if @mode.name is 'visual' and @mode.linewise
84 | [minRow, maxRow] = @adaptor.selectionRowRange()
85 | lines: (maxRow - minRow) + 1
86 | else
87 | @adaptor.characterwiseSelectionSize()
88 | @command.linewise = @mode.linewise
89 |
90 | @lastCommand = @command
91 |
92 | @command.visualExec this
93 | @command = null |
If we haven't changed out of characterwise visual mode and the direction 94 | of the selection changes, we have to make sure that the anchor character 95 | stays selected. | if @mode.name is 'visual' and not @mode.linewise
96 | if wasBackwards
97 | @adaptor.adjustAnchor -1 if not @adaptor.isSelectionBackwards()
98 | else
99 | @adaptor.adjustAnchor 1 if @adaptor.isSelectionBackwards() |
Other modes100 | 101 |Insert and replace modes just pass all keystrokes through (except | exports.insert = onKeypress: -> true
102 | exports.replace = onKeypress: -> true
103 |
104 | |
operators.coffee | |
---|---|
An operator followed by a motion is an | {Command} = require './helpers'
4 | {GoToLine, MoveToFirstNonBlank} = require './motions' |
The default key mappings are specified alongside the definitions of each
5 | | defaultMappings = {}
6 | map = (keys, operationClass) -> defaultMappings[keys] = operationClass |
Define the base class for all operations. | class Operation extends Command
7 | constructor: (@count = 1, @motion) ->
8 | @motion.operation = this if @motion
9 | isOperation: true
10 | isComplete: -> @motion?.isComplete()
11 | switchToMode: 'normal' |
Adjust the selection, if needed, and operate on that selection. | visualExec: (jim) ->
12 | if @linewise
13 | jim.adaptor.makeLinewise()
14 | else if not @motion?.exclusive
15 | jim.adaptor.includeCursorInSelection()
16 |
17 | @operate jim
18 |
19 | jim.setMode @switchToMode |
Select the amount of text that the motion moves over and operate on that 20 | selection. | exec: (jim) ->
21 | @startingPosition = jim.adaptor.position()
22 | jim.adaptor.setSelectionAnchor()
23 | if @count isnt 1
24 | @motion.count *= @count
25 | @count = 1
26 | @linewise ?= @motion.linewise
27 | @motion.exec jim
28 | @visualExec jim |
Change the selected text or the text that | map 'c', class Change extends Operation
30 | visualExec: (jim) ->
31 | super |
If we're repeating a | if @repeatableInsert
33 | jim.adaptor.insert @repeatableInsert.string
34 | jim.setMode 'normal' |
If we're executing this | else
36 | jim.afterInsertSwitch = true
37 |
38 | operate: (jim) -> |
If we're changing a linewise selection or motion, move the end of the 39 | previous line so that the cursor is left on an open line once the lines 40 | are deleted. | jim.adaptor.moveToEndOfPreviousLine() if @linewise
41 |
42 | jim.deleteSelection @motion?.exclusive, @linewise
43 |
44 | switchToMode: 'insert' |
Delete the selection or the text that | map 'd', class Delete extends Operation
45 | operate: (jim) ->
46 | jim.deleteSelection @motion?.exclusive, @linewise
47 | new MoveToFirstNonBlank().exec jim if @linewise |
Yank into a register the selection or the text that | map 'y', class Yank extends Operation
48 | operate: (jim) ->
49 | jim.yankSelection @motion?.exclusive, @linewise
50 | jim.adaptor.moveTo @startingPosition... if @startingPosition |
Indent the lines in the selection or the text that | map '>', class Indent extends Operation
51 | operate: (jim) ->
52 | [minRow, maxRow] = jim.adaptor.selectionRowRange()
53 | jim.adaptor.indentSelection()
54 | new GoToLine(minRow + 1).exec jim |
Outdent the lines in the selection or the text that | map '<', class Outdent extends Operation
55 | operate: (jim) ->
56 | [minRow, maxRow] = jim.adaptor.selectionRowRange()
57 | jim.adaptor.outdentSelection()
58 | new GoToLine(minRow + 1).exec jim
59 |
60 | module.exports = {Change, Delete, defaultMappings}
61 |
62 | |
33 | This is a Vim mode for Ace powered by Jim. To use Jim in Github's editor, drag this bookmarklet to your bookmarks bar: Please wait... 34 |
35 | 36 |38 | _.sortBy = function(obj, iterator, context) { 39 | return _.pluck(_.map(obj, function(value, index, list) { 40 | return { 41 | value : value, 42 | criteria : iterator.call(context, value, index, list) 43 | }; 44 | }).sort(function(left, right) { 45 | var a = left.criteria, b = right.criteria; 46 | return a < b ? -1 : a > b ? 1 : 0; 47 | }), 'value'); 48 | }; 49 | 50 | // borrowed from: 51 | // Underscore.js 1.1.6 52 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. 53 |54 |