Skip to content

Instantly share code, notes, and snippets.

@BonsaiDen
Created October 30, 2013 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BonsaiDen/7241976 to your computer and use it in GitHub Desktop.
Save BonsaiDen/7241976 to your computer and use it in GitHub Desktop.
Basic VIM Key Sequence Parser in JS
/**
* Copyright (c) 2013 Ivo Wetzel.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Parses VIM command sequences into object descriptions of the commands.
*
* Should support most of the basic functionality (i.e `ciw`, `20G`, `5dw`, `c5w`, `d3f.`)
*/
function Parser() {
this.buffer = [];
}
Parser.prototype = {
input: function(key) {
this.buffer.push(key);
var list = this.parseCommands(this.parseArguments(this.parseBuffer()));
this.stripBuffer(list);
list = this.cleanupCommands(list);
if (list.length) {
console.log('->', list);
}
return list;
},
is: function(template, cmd) {
return template.keys.split(' ').indexOf(cmd) !== -1;
},
parseBuffer: function() {
var index = 0,
repeatExp = /^([0-9]+)/,
count = null,
list = [];
while(index < this.buffer.length) {
var seq = this.buffer.slice(index).join(''),
chr = this.buffer[index];
// Try to match a repeat prefix
if (chr !== '0' && (count = seq.match(repeatExp))) {
index += count[1].length;
count = +count[1];
list.push({
count: count
});
// Pre-parse commands
} else {
var desc = {
cmd: chr,
args: {},
index: index
};
for(var type in Commands.plain) {
this.parseCommand(Commands.plain[type], desc);
}
for(type in Commands.withArgument) {
this.parseCommand(Commands.withArgument[type], desc);
}
list.push(desc);
index++;
}
}
return list;
},
parseCommand: function(template, desc) {
if (this.is(template, desc.cmd)) {
if (template.type) {
desc.type = template.type;
}
if (template.args) {
for(var i in template.args) {
desc.args[i] = template.args[i];
}
}
return true;
} else {
return false;
}
},
parseArguments: function(list) {
var index = 0;
while(index < list.length) {
var current = list[index],
next = list[index + 1];
// Repeat Arguments
if (!next) {
break;
} else if (current.count !== undefined
&& this.is(Commands.withArgument.number, next.cmd)) {
list.splice(index, 1);
next.args.number = current.count;
} else if (current.count !== undefined
&& this.is(Commands.plain.repeat, next.cmd)) {
list.splice(index, 1);
next.args.repeat = current.count;
// Range Arguments
} else if (this.is(Commands.withArgument.range, current.cmd)
&& this.is(Commands.arguments.range, next.cmd)) {
current.args.range = next;
list.splice(index + 1, 1);
index += 1;
// Character Arguments
} else if (this.is(Commands.withArgument.character, current.cmd)
&& next.cmd) {
current.args.character = next;
list.splice(index + 1, 1);
index += 1;
} else {
index++;
}
}
return list;
},
parseCommands: function(list) {
var index = 0,
parsed = [];
while(index < list.length) {
var current = list[index],
next = list[index + 1];
// Takes a movement its argument
if (next && this.is(Commands.withArgument.move, current.cmd)
&& this.is(Commands.plain.move, next.cmd)) {
current.args.move = next;
parsed.push(current);
index++;
// Takes itself as its argument
} else if (next && this.is(Commands.withArgument.self, current.cmd)
&& next.cmd === current.cmd) {
current.args.self = next;
parsed.push(current);
index++;
} else if (
// Commands with Number Arguments
(this.is(Commands.withArgument.number, current.cmd)
&& current.args.number !== null)
// Commands with Character Arguments
|| (this.is(Commands.withArgument.character, current.cmd)
&& current.args.character !== null)
// Movement Commands without Arguments
|| (this.is(Commands.plain.move, current.cmd))
// Commands with Range Arguments
|| (this.is(Commands.withArgument.range, current.cmd)
&& current.args.range !== null
&& current.args.move !== null)
// Commands without Arguments
|| (this.is(Commands.plain.single, current.cmd))
|| (this.is(Commands.plain.mode, current.cmd))
|| (this.is(Commands.plain.search, current.cmd))) {
parsed.push(current);
}
index++;
}
return parsed;
},
stripBuffer: function(list) {
var last = list[list.length - 1];
if (last) {
this.buffer.splice(
0,
Math.max(
last.index,
last.args.move ? last.args.move.index : 0,
last.args.range ? last.args.range.index : 0,
last.args.self ? last.args.self.index : 0
) + 1
);
}
},
cleanupCommands: function(list) {
return list.map(function(cmd) {
if (cmd.args.self) {
cmd.cmd += cmd.args.self.cmd;
delete cmd.args.self;
}
if (Object.keys(cmd.args).length === 0) {
delete cmd.args;
}
delete cmd.index;
return cmd;
});
},
};
/**
* Command Descriptors
*
* Modifier Keys are just like you would expect in VIM Script.
*
* `s-i` is basically `I` and `c-a` is `Control-a`
*/
var Commands = {
plain: {
// Commands which are considered movements
move: {
keys: 'w s-w e s-e b s-b f h j k l n s-n',
type: 'move'
},
// Commands which take a repeat argument infront of them
repeat: {
keys: 'w s-w e s-e b s-b f h j k l n s-n' + ' x r p s-p d y',
args: {
repeat: 1
}
},
// Single key commands
single: {
keys: '0 u c-r s-g ^ $ * o s-o s-d s-y',
type: 'single'
},
// Commands which change modes
mode: {
keys: 's-r i s-i a s-a v s-v c-v',
type: 'mode'
},
// Commands which enter search modes
search: {
keys: '/ ?',
type: 'search'
}
},
withArgument: {
// Commands which take movement commands ws their argument
move: {
keys: 'c d y v',
type: 'edit',
args: {
move: null
}
},
// Commands which take themselfs as arguments
self: {
keys: 'y s-y g d',
args: {
self: null
}
},
// Commands which take a characters as their argument
character: {
keys: 'f s-f',
args: {
character: null
}
},
// Commands which take a number as their argument
number: {
keys: 's-g',
args: {
number: null
}
},
// Commands which tak a range as their argument
range: {
keys: 'c d y',
args: {
range: null
}
}
},
arguments: {
// Arguments for the ranges
range: {
keys: 'a i " \' b B'
}
}
};
// Test -----------------------------------------------------------------------
var parser = new Parser();
function input(parser, characters) {
characters.split('').forEach(function(c) {
if ((/[A-Z]/).test(c)) {
parser.input('s-' + c.toLowerCase());
} else {
parser.input(c);
}
});
}
input(parser, '5dwd5w2fe20GggGugj5kciwdf 5NG0^$*/iaAd3f.');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment