├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── bin ├── build └── vimprint ├── ext └── ragel │ ├── .gitignore │ └── parser.rl ├── lib ├── vimprint.rb └── vimprint │ ├── .gitignore │ ├── command_registry.rb │ ├── core_ext │ ├── fixnum.rb │ └── string.rb │ ├── dsl.rb │ ├── formatters │ ├── base_formatter.rb │ ├── explainer.rb │ └── printer.rb │ ├── model │ ├── commands.rb │ ├── modes.rb │ └── stage.rb │ ├── registry.rb │ └── version.rb ├── localvimrc.vim ├── roadmap.md ├── test ├── examples │ ├── appending │ ├── autocomplete │ ├── helloworld │ └── starters │ │ ├── appending.js │ │ └── seashells.txt ├── vimprint │ ├── core_ext │ │ └── fixnum_test.rb │ ├── dsl_test.rb │ ├── formatters │ │ └── explainer_test.rb │ ├── model │ │ ├── commands_test.rb │ │ ├── modes_test.rb │ │ └── stage_test.rb │ ├── ragel │ │ └── parser_test.rb │ └── registry_test.rb └── vimprint_test.rb └── vimprint.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | notes 2 | notes-on-command-dsl.md 3 | *notes*.md 4 | scratch.rb 5 | *.log 6 | lib/vimprint/ragel 7 | next 8 | sketches 9 | ext/ragel/*.dot 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in vimprint.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | vimprint (0.0.1) 5 | nokogiri (~> 1.5) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | minitest (5.0.4) 11 | nokogiri (1.5.8) 12 | rake (10.0.3) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | minitest 19 | rake 20 | vimprint! 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Under construction!** 2 | 3 | For details about this project, watch [Vimprint - a Vim Keystroke Parser](https://vimeo.com/67215273), presented at [VimLondon](http://www.meetup.com/Vim-London/events/117155172/) on 28 May, 2013. 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/clean' 3 | require 'rake/testtask' 4 | 5 | CLEAN.include %w[ lib/vimprint/ragel ] 6 | 7 | desc "Generate a .dot visualization for each .rl file" 8 | desc "Compile each .rl file to .dot graph (viewable with Graphviz)" 9 | task :visualize do 10 | FileList.new('ext/ragel/*.rl').each do |file| 11 | system "ragel -Vp #{file} > #{file.ext('.dot')}" 12 | end 13 | end 14 | 15 | desc "Compile each .rl file to executable .rb" 16 | task :ragel => :clean do 17 | out_dir = File.expand_path('../lib/vimprint/ragel', __FILE__) 18 | Dir.mkdir(out_dir) unless Dir.exist?(out_dir) 19 | Dir.chdir('ext/ragel') do 20 | Dir['*.rl'].each do |file| 21 | out = file.sub(/rl$/, 'rb') 22 | sh "ragel -R #{file} -o #{out_dir}/#{out}" 23 | end 24 | end 25 | end 26 | 27 | Rake::TestTask.new(:test => :ragel) do |t| 28 | t.libs << 'test' 29 | t.test_files = FileList['test/**/*_test.rb'] 30 | t.verbose = true 31 | end 32 | 33 | desc "Run tests" 34 | task :default => :test 35 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system 'ragel -R lib/ragel/vim_parser.rl' 4 | system 'ragel -R lib/ragel/insert_parser.rl' 5 | -------------------------------------------------------------------------------- /bin/vimprint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'vimprint' 4 | log = ARGF.read 5 | $stderr.puts log.inspect if $DEBUG 6 | puts Vimprint.parse(log) 7 | -------------------------------------------------------------------------------- /ext/ragel/.gitignore: -------------------------------------------------------------------------------- 1 | vim_parser.rb 2 | insert_parser.rb 3 | simple.rb 4 | -------------------------------------------------------------------------------- /ext/ragel/parser.rl: -------------------------------------------------------------------------------- 1 | require 'vimprint/model/modes' 2 | require 'vimprint/model/stage' 3 | require 'vimprint/model/commands' 4 | 5 | module Vimprint 6 | 7 | %%{ 8 | machine parser; 9 | action H { @head = p; } 10 | action T { @tail = p; } 11 | 12 | tabkey = 9; 13 | enter = 13; 14 | ctrl_r = 18; 15 | ctrl_v = 22; 16 | escape = 27; 17 | 18 | abort = escape >H @T @{ @stage.add(:trigger, strokes) }; 19 | count = [1-9] >H @T @{ @stage.add(:count, strokes) }; 20 | register = ('"' [a-z]) >H @T @{ @stage.add(:register, strokes) }; 21 | count_register_prefix = (count? register)* count?; 22 | 23 | cut = [xX] >H @T @{ @stage.add(:trigger, strokes) }; 24 | cut_command = 25 | count_register_prefix 26 | ( 27 | cut @{ entry_point << RegisterCommand.new(@stage.commit) } 28 | | abort @{ entry_point << AbortedCommand.new(@stage.commit) } 29 | ); 30 | 31 | small_letter = [a-z] >H @T @{ @stage.add(:mark, strokes) }; 32 | big_letter = [A-Z] >H @T @{ @stage.add(:mark, strokes) }; 33 | mark = [m`] >H @T @{ @stage.add(:trigger, strokes) }; 34 | mark_command = 35 | count_register_prefix 36 | mark 37 | ( 38 | small_letter @{ entry_point << MarkCommand.new(@stage.commit) } 39 | | big_letter @{ entry_point << MarkCommand.new(@stage.commit) } 40 | | abort @{ entry_point << AbortedCommand.new(@stage.commit) } 41 | ); 42 | 43 | undo = 'u' >H @T @{ @stage.add(:trigger, strokes) }; 44 | redo = ctrl_r >H @T @{ @stage.add(:trigger, '') }; 45 | history_command = 46 | count_register_prefix 47 | (undo | redo) @{ entry_point << NormalCommand.new(@stage.commit) }; 48 | 49 | replace = 'r' >H @T @{ @stage.add(:trigger, strokes) }; 50 | printable_chars = (print | tabkey | enter) >H @T @{ @stage.add(:printable_char, strokes) }; 51 | replace_command = 52 | count_register_prefix 53 | replace 54 | ( 55 | printable_chars @{ entry_point << ReplaceCommand.new(@stage.commit) } 56 | | abort @{ entry_point << AbortedCommand.new(@stage.commit) } 57 | ); 58 | 59 | motion = [we] >H @T @{ @stage.add(:motion, strokes) }; 60 | motion_command = 61 | count_register_prefix 62 | motion @{ entry_point << MotionCommand.new(@stage.commit) }; 63 | 64 | onestroke_operator = [d>]; 65 | prefixed_operator = [?U]; 66 | twostroke_operator = 'g' prefixed_operator; 67 | operator = (onestroke_operator | twostroke_operator) >H @T @{ @stage.add(:operator, strokes) }; 68 | operator_echo = (onestroke_operator | twostroke_operator | prefixed_operator) >H @T @{ @stage.add(:operator, strokes) }; 69 | 70 | disallowed_in_operator_pending = (escape| tabkey | '"') >H @T @{ @stage.add(:trigger, strokes) }; 71 | operation = 72 | count_register_prefix 73 | operator 74 | count? 75 | ( 76 | motion @{ entry_point << Operation.build(@stage) } 77 | | operator_echo @{ entry_point << Operation.build(@stage) } 78 | | disallowed_in_operator_pending @{ entry_point << AbortedCommand.new(@stage.commit) } 79 | ); 80 | 81 | charwise_visual = 'v' >H @T @{ @stage.add(:switch, strokes) }; 82 | linewise_visual = 'V' >H @T @{ @stage.add(:switch, strokes) }; 83 | blockwise_visual = ctrl_v >H @T @{ @stage.add(:switch, strokes) }; 84 | lastwise_visual = 'gv' >H @T @{ @stage.add(:switch, strokes) }; 85 | 86 | start_charwise_visual_mode = 87 | charwise_visual @{ 88 | entry_point << (switch = VisualSwitch.new(@stage.commit)) 89 | @modestack.push(switch.commands) 90 | entry_point.nature = 'charwise' 91 | lastvisual = fentry(visual_charwise_mode); 92 | fcall visual_charwise_mode; 93 | }; 94 | 95 | start_linewise_visual_mode = 96 | linewise_visual @{ 97 | entry_point << (switch = VisualSwitch.new(@stage.commit)) 98 | @modestack.push(switch.commands) 99 | entry_point.nature = 'linewise' 100 | lastvisual = fentry(visual_linewise_mode); 101 | fcall visual_linewise_mode; 102 | }; 103 | 104 | start_blockwise_visual_mode = 105 | blockwise_visual @{ 106 | entry_point << (switch = VisualSwitch.new(@stage.commit)) 107 | @modestack.push(switch.commands) 108 | entry_point.nature = 'blockwise' 109 | lastvisual = fentry(visual_blockwise_mode); 110 | fcall visual_blockwise_mode; 111 | }; 112 | 113 | start_lastwise_visual_mode = 114 | lastwise_visual @{ 115 | entry_point << (switch = VisualSwitch.new(@stage.commit)) 116 | @modestack.push(switch.commands) 117 | fcall *lastvisual; 118 | }; 119 | 120 | visual_only_operator = [uU>]; 121 | visual_operator = (onestroke_operator | visual_only_operator) >H @T @{ @stage.add(:operator, strokes) }; 122 | 123 | visual_charwise_mode := ( 124 | ( 125 | (count? motion) @{ 126 | entry_point << MotionCommand.new(@stage.commit.merge(invocation_context: 'visual')) 127 | } 128 | )* 129 | ( 130 | visual_operator @{ 131 | entry_point << VisualOperation.new(@stage.commit) 132 | @modestack.pop 133 | fret; 134 | } 135 | | linewise_visual @{ 136 | entry_point << VisualSwitch.new(@stage.commit) 137 | entry_point.nature = 'linewise' 138 | lastvisual = fentry(visual_linewise_mode); 139 | fcall visual_linewise_mode; 140 | } 141 | | blockwise_visual @{ 142 | entry_point << VisualSwitch.new(@stage.commit) 143 | entry_point.nature = 'blockwise' 144 | lastvisual = fentry(visual_blockwise_mode); 145 | fcall visual_blockwise_mode; 146 | } 147 | | (abort | charwise_visual) @{ 148 | entry_point << Terminator.new(@stage.commit) 149 | @modestack.pop 150 | fret; 151 | } 152 | ) 153 | ); 154 | 155 | visual_linewise_mode := ( 156 | ( 157 | visual_operator @{ 158 | entry_point << VisualOperation.new(@stage.commit) 159 | @modestack.pop 160 | fret; 161 | } 162 | | charwise_visual @{ 163 | entry_point << VisualSwitch.new(@stage.commit) 164 | entry_point.nature = 'charwise' 165 | lastvisual = fentry(visual_charwise_mode); 166 | fcall visual_charwise_mode; 167 | } 168 | | blockwise_visual @{ 169 | entry_point << VisualSwitch.new(@stage.commit) 170 | entry_point.nature = 'blockwise' 171 | lastvisual = fentry(visual_blockwise_mode); 172 | fcall visual_blockwise_mode; 173 | } 174 | | (abort | linewise_visual) @{ 175 | entry_point << Terminator.new(@stage.commit) 176 | @modestack.pop 177 | fret; 178 | } 179 | ) 180 | ); 181 | 182 | visual_blockwise_mode := ( 183 | charwise_visual @{ 184 | entry_point << VisualSwitch.new(@stage.commit) 185 | entry_point.nature = 'charwise' 186 | lastvisual = fentry(visual_charwise_mode); 187 | fcall visual_charwise_mode; 188 | } 189 | | linewise_visual @{ 190 | entry_point << VisualSwitch.new(@stage.commit) 191 | entry_point.nature = 'linewise' 192 | lastvisual = fentry(visual_linewise_mode); 193 | fcall visual_linewise_mode; 194 | } 195 | | (abort | blockwise_visual) @{ 196 | entry_point << Terminator.new(@stage.commit) 197 | @modestack.pop 198 | fret; 199 | } 200 | )*; 201 | 202 | normal := ( 203 | cut_command | 204 | mark_command | 205 | history_command | 206 | replace_command | 207 | motion_command | 208 | operation | 209 | start_charwise_visual_mode | 210 | start_linewise_visual_mode | 211 | start_blockwise_visual_mode | 212 | start_lastwise_visual_mode 213 | )*; 214 | 215 | }%% 216 | 217 | class Parser 218 | 219 | attr_accessor :data 220 | 221 | def initialize(listener=[]) 222 | @eventlist = listener 223 | @modestack = [@eventlist] 224 | @stage = Stage.new 225 | %% write data; 226 | end 227 | 228 | def process(input) 229 | @data = input.unpack("c*") 230 | stack = [] 231 | %% write init; 232 | %% write exec; 233 | @eventlist 234 | end 235 | 236 | def entry_point 237 | @modestack.last 238 | end 239 | 240 | def strokes 241 | keystrokes(@data[@head..@tail].pack('c*')) 242 | end 243 | 244 | def keystrokes(input) 245 | input 246 | .gsub(/ /, '') 247 | .gsub(/\t/, '') 248 | .gsub(/\r/, '') 249 | .gsub(/\e/, '') 250 | .gsub(/\x16/, '') 251 | end 252 | 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /lib/vimprint.rb: -------------------------------------------------------------------------------- 1 | require "vimprint/version" 2 | require "vimprint/formatters/printer" 3 | require "vimprint/formatters/explainer" 4 | require "vimprint/ragel/parser" 5 | 6 | module Vimprint 7 | 8 | def self.explain(keystrokes) 9 | Explainer.new(process(keystrokes)).explain 10 | end 11 | 12 | def self.pp(keystrokes) 13 | Printer.new(process(keystrokes)).print 14 | end 15 | 16 | private 17 | 18 | def self.process(keystrokes) 19 | NormalMode.new.tap do |eventlist| 20 | Parser.new(eventlist).process(keystrokes) 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/vimprint/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelstrom/vimprint/201e3d584992694984b5f918c02b70d3ec0d8ab9/lib/vimprint/.gitignore -------------------------------------------------------------------------------- /lib/vimprint/command_registry.rb: -------------------------------------------------------------------------------- 1 | require_relative 'registry' 2 | 3 | module Vimprint 4 | 5 | @normal_mode = Registry.create_mode("normal") 6 | @visual_mode = Registry.create_mode("visual") 7 | 8 | # Sample DSL for generating these explanations: 9 | # command { 10 | # trigger 'x' 11 | # explain { 12 | # template 'cut {{number}} {{register}}' 13 | # number { 14 | # singular 'character under cursor' 15 | # plural '#{count} characters' 16 | # } 17 | # register { 18 | # default 'into default register' 19 | # named 'into register #{register}' 20 | # uppercase 'and append into register #{register}' 21 | # } 22 | # } 23 | # } 24 | @normal_mode.create_command( 25 | {trigger: 'x', number: 'singular', register: 'default'}, 26 | 'cut character under cursor into default register') 27 | @normal_mode.create_command( 28 | {trigger: 'x', number: 'plural', register: 'default'}, 29 | 'cut #{count} characters into default register') 30 | @normal_mode.create_command( 31 | {trigger: 'x', number: 'singular', register: 'named'}, 32 | 'cut character under cursor into register #{register}') 33 | @normal_mode.create_command( 34 | {trigger: 'x', number: 'plural', register: 'named'}, 35 | 'cut #{count} characters into register #{register}') 36 | 37 | # Sample DSL for generating these explanations: 38 | # command { 39 | # trigger 'X' 40 | # explain { 41 | # template 'cut {{number}} {{register}}' 42 | # number { 43 | # singular '1 character before cursor' 44 | # plural '#{count} characters before cursor' 45 | # } 46 | # register { 47 | # default 'into default register' 48 | # named 'into register #{register}' 49 | # uppercase 'and append into register #{register}' 50 | # } 51 | # } 52 | # } 53 | @normal_mode.create_command( 54 | {trigger: 'X', number: 'singular', register: 'default'}, 55 | 'cut 1 character before cursor into default register') 56 | @normal_mode.create_command( 57 | {trigger: 'X', number: 'plural', register: 'default'}, 58 | 'cut #{count} characters before cursor into default register') 59 | @normal_mode.create_command( 60 | {trigger: 'X', number: 'singular', register: 'named'}, 61 | 'cut 1 character before cursor into register #{register}') 62 | @normal_mode.create_command( 63 | {trigger: 'X', number: 'plural', register: 'named'}, 64 | 'cut #{count} characters before cursor into register #{register}') 65 | 66 | @normal_mode.create_command( 67 | {trigger: 'm', mark: 'lowercase'}, 68 | 'save current position with local mark #{mark}') 69 | @normal_mode.create_command( 70 | {trigger: 'm', mark: 'uppercase'}, 71 | 'save current position with global mark #{mark}') 72 | 73 | @normal_mode.create_command( 74 | {trigger: '`', mark: 'lowercase'}, 75 | 'jump to local mark #{mark}') 76 | @normal_mode.create_command( 77 | {trigger: '`', mark: 'uppercase'}, 78 | 'jump to global mark #{mark}') 79 | 80 | @normal_mode.create_command( 81 | {trigger: 'u', number: 'singular'}, 82 | 'undo 1 change') 83 | @normal_mode.create_command( 84 | {trigger: 'u', number: 'plural'}, 85 | 'undo #{count} changes') 86 | 87 | @normal_mode.create_command( 88 | {trigger: '', number: 'singular'}, 89 | 'redo 1 change') 90 | @normal_mode.create_command( 91 | {trigger: '', number: 'plural'}, 92 | 'redo #{count} changes') 93 | 94 | @normal_mode.create_command( 95 | {trigger: 'r', number: 'singular', printable_char: true}, 96 | 'replace current character with #{printable_char}') 97 | @normal_mode.create_command( 98 | {trigger: 'r', number: 'plural', printable_char: true}, 99 | 'replace next #{count} characters with #{printable_char}') 100 | 101 | @normal_mode.create_command( 102 | {aborted: true}, 103 | '[aborted command]') 104 | 105 | Registry.create_motion( 106 | {motion: 'w', number: 'singular'}, 107 | 'to start of next word') 108 | Registry.create_motion( 109 | {motion: 'w', number: 'plural'}, 110 | 'to start of #{count.ordinalize} word') 111 | Registry.create_motion( 112 | {motion: 'e', number: 'singular'}, 113 | 'to end of word') 114 | Registry.create_motion( 115 | {motion: 'e', number: 'plural'}, 116 | 'to end of #{count.ordinalize} word') 117 | 118 | Registry.create_operator({trigger: 'd'}, 'delete') 119 | Registry.create_operator({trigger: '>'}, 'indent') 120 | Registry.create_operator({trigger: 'g?'}, 'rot13 encode') 121 | Registry.create_operator({trigger: 'gU'}, 'upcase') 122 | 123 | # NOTE: these operators only occur in Visual mode 124 | Registry.create_operator({trigger: 'u'}, 'downcase') 125 | Registry.create_operator({trigger: 'U'}, 'upcase') 126 | 127 | @normal_mode.create_command( 128 | {switch: 'v'}, 129 | 'start Visual mode charwise') 130 | @normal_mode.create_command( 131 | {switch: 'V'}, 132 | 'start Visual mode linewise') 133 | @normal_mode.create_command( 134 | {switch: ''}, 135 | 'start Visual mode blockwise') 136 | @normal_mode.create_command( 137 | {switch: 'gv'}, 138 | 'start Visual mode and reselect previous selection') 139 | 140 | @visual_mode.create_command( 141 | {pop: true}, 142 | 'pop to Normal mode') 143 | @visual_mode.create_command( 144 | {switch: 'v'}, 145 | 'change to Visual mode charwise') 146 | @visual_mode.create_command( 147 | {switch: 'V'}, 148 | 'change to Visual mode linewise') 149 | @visual_mode.create_command( 150 | {switch: ''}, 151 | 'change to Visual mode blockwise') 152 | 153 | end 154 | -------------------------------------------------------------------------------- /lib/vimprint/core_ext/fixnum.rb: -------------------------------------------------------------------------------- 1 | class Fixnum 2 | def ordinalize 3 | if (11..13).include?(self.to_i % 100) 4 | "#{self}th" 5 | else 6 | case self.to_i % 10 7 | when 1; "#{self}st" 8 | when 2; "#{self}nd" 9 | when 3; "#{self}rd" 10 | else "#{self}th" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/vimprint/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def substitute(binding=TOPLEVEL_BINDING) 3 | eval(%{"#{self}"}, binding) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/vimprint/dsl.rb: -------------------------------------------------------------------------------- 1 | require_relative 'registry' 2 | 3 | module Vimprint 4 | class Config 5 | attr_reader :config 6 | 7 | def initialize(&block) 8 | @config = HashFromBlock.build(&block) 9 | end 10 | 11 | def signature 12 | { trigger: config[:trigger] } 13 | end 14 | 15 | def template 16 | config[:explain][:template] 17 | end 18 | 19 | def projected_templates 20 | config[:explain][:number].each.with_object({}) do |(key,value),hash| 21 | hash[key.to_sym] = template.sub('{{number}}', value) 22 | end 23 | end 24 | end 25 | 26 | class HashFromBlock 27 | attr_reader :hash 28 | 29 | def self.build(&block) 30 | self.new(&block).hash 31 | end 32 | 33 | def initialize(&block) 34 | @hash = {} 35 | instance_eval(&block) 36 | end 37 | 38 | def method_missing(name, *args, &block) 39 | args = args.shift if args.size == 1 40 | hash[name] = (block.nil?) ? args : self.class.build(&block) 41 | end 42 | end 43 | 44 | module Dsl 45 | class << self 46 | attr_accessor :current_mode 47 | end 48 | 49 | def self.parse(&block) 50 | self.current_mode = Registry.create_mode("normal") 51 | self.module_eval(&block) 52 | end 53 | 54 | def self.motion(&block) 55 | config = Config.new(&block) 56 | signature = config.signature 57 | 58 | config.projected_templates.each do |number,template| 59 | current_mode.create_command( 60 | signature.merge(number: number.to_s), 61 | template 62 | ) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/vimprint/formatters/base_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'vimprint/command_registry' 2 | 3 | module Vimprint 4 | class BaseFormatter 5 | attr_reader :commands 6 | 7 | def initialize(commands) 8 | @commands = commands 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/vimprint/formatters/explainer.rb: -------------------------------------------------------------------------------- 1 | require 'vimprint/formatters/base_formatter' 2 | require 'vimprint/model/modes' 3 | require 'vimprint/model/commands' 4 | 5 | module Vimprint 6 | 7 | class Couple < Struct.new(:keystrokes, :explanation) 8 | def to_a 9 | [keystrokes, explanation] 10 | end 11 | end 12 | 13 | class Explainer < BaseFormatter 14 | def explain 15 | commands.explain 16 | end 17 | end 18 | 19 | class NormalMode 20 | def explain 21 | map { |o| o.explain("normal") }.flatten.map(&:to_a) 22 | end 23 | end 24 | 25 | class VisualMode 26 | def explain(context) 27 | map { |o| o.explain("visual") } 28 | end 29 | end 30 | 31 | class BaseCommand 32 | def explain(context) 33 | Couple.new(raw_keystrokes, lookup(context)) 34 | end 35 | 36 | def lookup(context) 37 | Registry.lookup(context, signature).render(binding) 38 | end 39 | end 40 | 41 | class BareMotion 42 | def lookup(context) 43 | Registry.get_motion(signature).render(binding).strip 44 | end 45 | end 46 | 47 | class MotionCommand 48 | def lookup(context) 49 | [verb, super].compact.join(" ") 50 | end 51 | end 52 | 53 | class Echo 54 | def lookup(context) 55 | count > 1 ? "#{count} lines" : "a line" 56 | end 57 | end 58 | 59 | class Operator 60 | def lookup(context) 61 | Registry.get_operator(signature) 62 | end 63 | end 64 | 65 | class Operation 66 | def lookup(context) 67 | [operator.lookup(context), extent.lookup(context)].join(" ") 68 | end 69 | end 70 | 71 | class VisualSwitch 72 | def explain(context) 73 | [ 74 | super, 75 | [commands.explain(context)].flatten 76 | ] 77 | end 78 | end 79 | 80 | class Terminator 81 | def lookup(context) 82 | Registry.lookup(context, signature).render(binding).strip 83 | end 84 | end 85 | 86 | class VisualOperation 87 | def lookup(context) 88 | [operator.lookup(context), selection].join(" ") 89 | end 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /lib/vimprint/formatters/printer.rb: -------------------------------------------------------------------------------- 1 | require 'vimprint/formatters/base_formatter' 2 | 3 | module Vimprint 4 | class Printer < BaseFormatter 5 | def print 6 | commands.print 7 | end 8 | end 9 | 10 | class NormalMode 11 | def print 12 | map(&:print).join 13 | end 14 | end 15 | 16 | class NormalCommand 17 | def print 18 | raw_keystrokes + " " 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/vimprint/model/commands.rb: -------------------------------------------------------------------------------- 1 | module Vimprint 2 | 3 | module ReceivesCount 4 | def plurality 5 | return 'singular' if @count.nil? 6 | @count > 1 ? 'plural' : 'singular' 7 | end 8 | end 9 | 10 | class BaseCommand 11 | attr_accessor :container 12 | attr_reader :trigger, :count, :raw_keystrokes, :register, :mark 13 | 14 | def initialize(config={}) 15 | @trigger = config[:trigger] 16 | @count = config[:count] 17 | @register = config[:register] 18 | @motion = config[:motion] 19 | @mark = config[:mark] 20 | @switch = config[:switch] 21 | @operator = config[:operator] 22 | @raw_keystrokes = config[:raw_keystrokes] 23 | @printable_char = config[:printable_char] 24 | end 25 | 26 | def ==(other) 27 | @trigger == other.trigger && 28 | @count == other.count && 29 | @raw_keystrokes == other.raw_keystrokes 30 | end 31 | 32 | def signature 33 | {} 34 | end 35 | 36 | end 37 | 38 | class NormalCommand < BaseCommand 39 | include ReceivesCount 40 | def signature 41 | super.merge({ 42 | number: plurality, 43 | trigger: trigger 44 | }) 45 | end 46 | end 47 | 48 | class RegisterCommand < NormalCommand 49 | def signature 50 | super.merge({register: register_description}) 51 | end 52 | 53 | def register_description 54 | return 'default' if (@register.nil? || @register.empty?) 55 | "named" 56 | end 57 | end 58 | 59 | class MarkCommand < BaseCommand 60 | def signature 61 | super.merge({ 62 | mark: mark_description, 63 | trigger: trigger 64 | }) 65 | end 66 | 67 | def mark_description 68 | /[[:upper:]]/.match(@mark) ? "uppercase" : "lowercase" 69 | end 70 | end 71 | 72 | class ReplaceCommand < NormalCommand 73 | attr_accessor :printable_char 74 | def signature 75 | super.merge({printable_char: true}) 76 | end 77 | end 78 | 79 | class AbortedCommand < BaseCommand 80 | def signature 81 | { aborted: true } 82 | end 83 | end 84 | 85 | class BareMotion < BaseCommand 86 | include ReceivesCount 87 | attr_accessor :motion 88 | def signature 89 | super.merge({ 90 | number: plurality, 91 | motion: motion 92 | }) 93 | end 94 | def invocation_context 95 | container.class.name.split('::').last 96 | end 97 | end 98 | 99 | class MotionCommand < BareMotion 100 | def verb 101 | case invocation_context 102 | when "VisualMode" then "select" 103 | when "NormalMode" then "move forward" 104 | end 105 | end 106 | end 107 | 108 | class Operator 109 | attr_accessor :trigger, :register 110 | def initialize(config={}) 111 | @trigger = config[:trigger] 112 | @register = config[:register] 113 | end 114 | def signature 115 | {trigger: @trigger} 116 | end 117 | def ==(other) 118 | @trigger == other.trigger && 119 | @register == other.register 120 | end 121 | end 122 | 123 | class Extent 124 | def self.build(config) 125 | if config.has_key?(:echo) && config[:echo] != '' 126 | Echo.new({ 127 | trigger: config[:echo], 128 | count: config[:count] 129 | }) 130 | else 131 | BareMotion.new({ 132 | motion: config[:motion], 133 | count: config[:count], 134 | }) 135 | end 136 | end 137 | end 138 | 139 | class Echo 140 | attr_reader :trigger, :count 141 | def initialize(config={}) 142 | @trigger = config[:trigger] 143 | @count = config.fetch(:count, 1).to_i 144 | end 145 | def ==(other) 146 | @trigger == other.trigger 147 | end 148 | end 149 | 150 | class Operation < BaseCommand 151 | attr_accessor :operator, :extent 152 | 153 | def self.build(stage) 154 | if stage.echo_is_true? || stage.motion != '' 155 | Operation.new(stage.commit) 156 | else 157 | AbortedCommand.new(stage.commit) 158 | end 159 | end 160 | 161 | def initialize(config={}) 162 | @raw_keystrokes = config[:raw_keystrokes] 163 | @operator = Operator.new({ 164 | trigger: config[:operator], 165 | register: config[:register] 166 | }) 167 | @extent = Extent.build(config) 168 | end 169 | end 170 | 171 | class VisualSwitch < BaseCommand 172 | attr_reader :switch, :commands 173 | def initialize(config={}) 174 | super 175 | @commands = VisualMode.new 176 | end 177 | def signature 178 | super.merge({ switch: switch }) 179 | end 180 | end 181 | 182 | class Terminator < BaseCommand 183 | def signature 184 | super.merge({ pop: true }) 185 | end 186 | end 187 | 188 | class VisualOperation < Terminator 189 | attr_accessor :operator 190 | def initialize(config={}) 191 | super 192 | @operator = Operator.new({ 193 | trigger: config[:operator], 194 | }) 195 | end 196 | def signature 197 | super.merge({operator: operator}) 198 | end 199 | def selection 200 | if @operator.trigger == '>' 201 | return "selection" 202 | end 203 | case container.nature 204 | when 'charwise' then 'selected characters' 205 | when 'linewise' then 'selected lines' 206 | else "selection" 207 | end 208 | end 209 | end 210 | 211 | end 212 | -------------------------------------------------------------------------------- /lib/vimprint/model/modes.rb: -------------------------------------------------------------------------------- 1 | module Vimprint 2 | 3 | class BaseMode 4 | 5 | extend Forwardable 6 | def_delegators :@events, :size, :map 7 | attr_accessor :events 8 | 9 | def initialize(*events) 10 | events.each { |e| reciprocate(e) } 11 | @events = events 12 | end 13 | 14 | def <<(event) 15 | reciprocate(event) 16 | @events << event 17 | end 18 | 19 | private 20 | 21 | def reciprocate(event) 22 | if event.respond_to?(:container=) 23 | event.container = self 24 | end 25 | end 26 | 27 | end 28 | 29 | class NormalMode < BaseMode 30 | end 31 | 32 | class VisualMode < BaseMode 33 | attr_accessor :nature 34 | def initialize(nature="charwise", *events) 35 | super(*events) 36 | @nature = nature 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/vimprint/model/stage.rb: -------------------------------------------------------------------------------- 1 | module Vimprint 2 | class Stage 3 | 4 | attr_reader :register, :motion, :counts 5 | 6 | def initialize() 7 | reset 8 | end 9 | 10 | def reset 11 | @buffer = [] 12 | @counts = [] 13 | @operators = [] 14 | string_instance_variables.each do |name| 15 | instance_variable_set("@#{name}", "") 16 | end 17 | end 18 | 19 | def commit 20 | to_hash.tap { reset } 21 | end 22 | 23 | def string_instance_variables 24 | [:register, :trigger, :mark, :switch, :motion, :printable_char] 25 | end 26 | 27 | def hash_of_string_instance_variables 28 | Hash[string_instance_variables.map { |name| 29 | [name, instance_variable_get("@#{name}")] 30 | }] 31 | end 32 | 33 | def to_hash 34 | hash_of_string_instance_variables.merge({ 35 | raw_keystrokes: raw_keystrokes, 36 | count: effective_count, 37 | operator: operator, 38 | echo: echo, 39 | }).reject do |k,v| 40 | v.nil? || v == [] || v == "" 41 | end 42 | end 43 | 44 | def echo 45 | @operators[1] 46 | end 47 | 48 | def operator 49 | @operators[0] 50 | end 51 | 52 | def echo_is_true? 53 | operator == echo || operator[-1] == echo 54 | end 55 | 56 | def effective_count 57 | @counts.map { |digit| digit.to_i }.inject(:*) 58 | end 59 | 60 | alias_method :count, :effective_count 61 | 62 | def raw_keystrokes 63 | @buffer.join 64 | end 65 | 66 | def add(name, value) 67 | case name 68 | when :register then @register = value.sub(/^"/, '') 69 | when :count then @counts << value 70 | when :operator then @operators << value 71 | else 72 | instance_variable_set("@#{name}", value) 73 | end 74 | @buffer << value 75 | end 76 | 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /lib/vimprint/registry.rb: -------------------------------------------------------------------------------- 1 | require_relative 'core_ext/fixnum' 2 | require_relative 'core_ext/string' 3 | 4 | module Vimprint 5 | 6 | class NoModeError < NameError; end 7 | class NoCommandError < NameError; end 8 | class NoOperatorError < NameError; end 9 | class NoMotionError < NameError; end 10 | 11 | class Explanation < Struct.new(:template) 12 | def render(context) 13 | template.substitute(context) 14 | end 15 | end 16 | 17 | class Registry 18 | 19 | def self.lookup(mode, signature) 20 | Registry.get_mode(mode).get_command(signature) 21 | end 22 | 23 | def self.get_mode(name) 24 | @modes.fetch(name) { 25 | raise NoModeError.new("Vimprint doesn't know about #{name} mode") 26 | } 27 | end 28 | 29 | def self.create_mode(name) 30 | @modes ||= {} 31 | @modes[name] ||= new 32 | end 33 | 34 | def self.get_operator(name) 35 | @operators.fetch(name) { 36 | raise NoOperatorError.new("no match found for operator: #{name}") 37 | } 38 | end 39 | 40 | def self.create_operator(name, verb) 41 | @operators ||= {} 42 | @operators[name] = verb 43 | end 44 | 45 | def self.get_motion(signature) 46 | @motions.fetch(signature) { 47 | raise NoMotionError.new("no match found for motion: #{signature}") 48 | } 49 | end 50 | 51 | def self.create_motion(signature, template) 52 | @motions ||= {} 53 | @motions[signature] = Explanation.new(template) 54 | end 55 | 56 | def create_command(signature, template) 57 | @commands ||= {} 58 | @commands[signature] = Explanation.new(template) 59 | end 60 | 61 | def get_command(signature) 62 | @commands.fetch(signature) { 63 | raise NoCommandError.new("no Explanation found for command signature: #{signature}") 64 | } 65 | end 66 | 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /lib/vimprint/version.rb: -------------------------------------------------------------------------------- 1 | module Vimprint 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /localvimrc.vim: -------------------------------------------------------------------------------- 1 | if has("autocmd") 2 | autocmd FileType ruby call SetupTestrunner() 3 | endif 4 | 5 | function! SetupTestrunner() 6 | let name = expand('%:t') 7 | if match(name, '_test') > -1 8 | nnoremap Q :wa :!ruby % 9 | else 10 | nnoremap Q :execute ':wa :!ruby' rake#buffer().related() 11 | endif 12 | endfunction 13 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap for Vimprint 2 | 3 | Vimprint is currently under construction. Here are some of the areas that need work, in approximate order of priority: 4 | 5 | - [ ] create a Ruby API for building an AST that models Vim's commands 6 | - [ ] create a formatter that prints each Vim command, with whitespace/newlines to separate them 7 | - [ ] create a formatter that explains each Vim command (in the style of [Vimsplain][]) 8 | - [ ] build a Ragel parser for processing Vim keystrokes, turning them into an AST 9 | - [ ] create a Ruby DSL for specifying Vim commands with metadata: start/end mode, trigger, explanation, etc. 10 | - [ ] handle input - capture keystrokes and forward them to Vim and Vimprint 11 | - [ ] handle output - make Vimprint a fullscreen terminal app, using [curses][]/[ncurses][] 12 | - [ ] use the Ruby DSL to specify all of Vim's built-in commands 13 | 14 | [curses]: http://www.ruby-doc.org/stdlib-2.0/libdoc/curses/rdoc/Curses.html 15 | [ncurses]: http://ncurses-ruby.berlios.de/ 16 | [Vimsplain]: https://github.com/pafcu/Vimsplain 17 | -------------------------------------------------------------------------------- /test/examples/appending: -------------------------------------------------------------------------------- 1 | A;e.e.e.:q! -------------------------------------------------------------------------------- /test/examples/autocomplete: -------------------------------------------------------------------------------- 1 | :e test ex s s A s shore:q! -------------------------------------------------------------------------------- /test/examples/helloworld: -------------------------------------------------------------------------------- 1 | IHello, World! 2 | -------------------------------------------------------------------------------- /test/examples/starters/appending.js: -------------------------------------------------------------------------------- 1 | var foo 2 | 3 | var bar = myCoolStuff() 4 | 5 | callRemote() 6 | 7 | foo = callTheWorld() 8 | -------------------------------------------------------------------------------- /test/examples/starters/seashells.txt: -------------------------------------------------------------------------------- 1 | she sells sea shells by the 2 | -------------------------------------------------------------------------------- /test/vimprint/core_ext/fixnum_test.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/autorun' 3 | require 'minitest/pride' 4 | require './lib/vimprint/core_ext/fixnum' 5 | 6 | describe 'Fixnum#ordinalize' do 7 | 8 | it 'uses -st suffix for numbers ending 1' do 9 | assert_equal '1st', 1.ordinalize 10 | assert_equal '21st', 21.ordinalize 11 | assert_equal '101st', 101.ordinalize 12 | end 13 | 14 | it 'uses -nd suffix for numbers ending 2' do 15 | assert_equal '2nd', 2.ordinalize 16 | assert_equal '22nd', 22.ordinalize 17 | assert_equal '102nd', 102.ordinalize 18 | end 19 | 20 | it 'uses -nd suffix for numbers ending 3' do 21 | assert_equal '3rd', 3.ordinalize 22 | assert_equal '23rd', 23.ordinalize 23 | assert_equal '103rd', 103.ordinalize 24 | end 25 | 26 | it 'uses -th suffix for 11 and 12' do 27 | assert_equal '11th', 11.ordinalize 28 | assert_equal '12th', 12.ordinalize 29 | assert_equal '511th', 511.ordinalize 30 | assert_equal '512th', 512.ordinalize 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /test/vimprint/dsl_test.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/autorun' 3 | require 'minitest/pride' 4 | require './lib/vimprint/dsl' 5 | 6 | module Vimprint 7 | 8 | describe HashFromBlock do 9 | 10 | def values 11 | proc do 12 | one 1 13 | two 2 14 | nested do 15 | three 3 16 | end 17 | end 18 | end 19 | 20 | it '.new returns a HashFromBlock object' do 21 | builder = HashFromBlock.new &values 22 | assert_equal HashFromBlock, builder.class 23 | assert_equal({one: 1, two: 2, nested: {three: 3}}, builder.hash) 24 | end 25 | 26 | it 'can add new items' do 27 | builder = HashFromBlock.new { one 1 } 28 | builder.two 2 29 | assert_equal({one: 1, two: 2}, builder.hash) 30 | end 31 | 32 | it '.build returns a hash object' do 33 | hash = HashFromBlock.build &values 34 | assert_equal({one: 1, two: 2, nested: {three: 3}}, hash) 35 | end 36 | 37 | end 38 | 39 | describe Config do 40 | 41 | def config 42 | Config.new do 43 | trigger 'h' 44 | explain { 45 | template 'move left {{number}}' 46 | number { 47 | singular "1 character" 48 | plural '#{count} characters' 49 | } 50 | } 51 | end 52 | end 53 | 54 | it '#signature accesses the trigger' do 55 | assert_equal({trigger: "h"}, config.signature) 56 | end 57 | 58 | it '#template accesses the template' do 59 | assert_equal('move left {{number}}', config.template) 60 | end 61 | 62 | it '#projected_templates gets singular+plural templates' do 63 | templates = { 64 | :singular => "move left 1 character", 65 | :plural => 'move left #{count} characters', 66 | } 67 | assert_equal(templates, config.projected_templates) 68 | end 69 | 70 | end 71 | 72 | describe 'Dsl.parse' do 73 | 74 | before do 75 | Dsl.parse do 76 | motion { 77 | trigger 'h' 78 | explain { 79 | template 'move left {{number}}' 80 | number { 81 | singular "1 character" 82 | plural '#{count} characters' 83 | } 84 | } 85 | } 86 | end 87 | end 88 | 89 | def normal_mode 90 | Registry.get_mode("normal") 91 | end 92 | 93 | it 'motion block generates a singular explanation' do 94 | h_once = normal_mode.get_command({trigger: 'h', number: 'singular'}) 95 | assert_equal Explanation, h_once.class 96 | assert_equal "move left 1 character", h_once.template 97 | end 98 | 99 | it 'motion block generates a plural explanation' do 100 | h_multiple = normal_mode.get_command({trigger: 'h', number: 'plural'}) 101 | assert_equal Explanation, h_multiple.class 102 | assert_equal 'move left #{count} characters', h_multiple.template 103 | end 104 | 105 | end 106 | 107 | end 108 | -------------------------------------------------------------------------------- /test/vimprint/formatters/explainer_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'vimprint/formatters/explainer' 3 | 4 | module Vimprint 5 | 6 | describe Couple do 7 | it 'returns array: [keystrokes, explanation]' do 8 | pair = Couple.new('k', 'move up') 9 | assert_equal ['k', 'move up'], pair.to_a 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /test/vimprint/model/commands_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'vimprint/model/commands' 3 | 4 | module Vimprint 5 | 6 | describe BaseCommand do 7 | it 'can set/get :container' do 8 | command = BaseCommand.new 9 | assert_equal nil, command.container 10 | command.container = (list = []) 11 | assert_equal list, command.container 12 | end 13 | end 14 | 15 | describe NormalCommand do 16 | it 'can describe its plurality' do 17 | assert_equal 'singular', NormalCommand.new.plurality 18 | assert_equal 'singular', NormalCommand.new({count: 1}).plurality 19 | assert_equal 'plural', NormalCommand.new({count: 2}).plurality 20 | end 21 | end 22 | 23 | describe RegisterCommand do 24 | it 'can describe its register' do 25 | assert_equal 'default', RegisterCommand.new.register_description 26 | assert_equal 'named', RegisterCommand.new({register: 'a'}).register_description 27 | end 28 | end 29 | 30 | describe MotionCommand do 31 | 32 | it 'assumes it\'s being called from Normal mode' do 33 | motion = MotionCommand.new({ 34 | raw_keystrokes: 'w', 35 | motion: 'w' 36 | }) 37 | normal = NormalMode.new 38 | normal << motion 39 | assert_equal 'NormalMode', motion.invocation_context 40 | assert_equal 'move forward', motion.verb 41 | end 42 | 43 | it 'knows when it\'s being called from Visual mode' do 44 | motion = MotionCommand.new({ 45 | raw_keystrokes: 'w', 46 | motion: 'w' 47 | }) 48 | visual = VisualMode.new 49 | visual << motion 50 | assert_equal 'VisualMode', motion.invocation_context 51 | assert_equal 'select', motion.verb 52 | end 53 | 54 | 55 | end 56 | 57 | describe Operator do 58 | it 'has a trigger value' do 59 | operator = Operator.new({trigger: 'd'}) 60 | assert_equal 'd', operator.trigger 61 | end 62 | end 63 | 64 | describe Extent do 65 | it 'creates a motion from config' do 66 | extent = Extent.build({motion: 'w', count: '2'}) 67 | assert_equal BareMotion.new({motion: 'w', count: '2'}), extent 68 | end 69 | it 'creates an echo from config' do 70 | extent = Extent.build({echo: 'd'}) 71 | assert_equal Echo.new({trigger: 'd'}), extent 72 | end 73 | it 'creates a text object from config' do 74 | skip "FILL THIS OUT WHEN IMPLEMENTING TEXT OBJECTS" 75 | end 76 | end 77 | 78 | describe Operation do 79 | 80 | it 'can be constructed from operator + motion' do 81 | operation = Operation.new({ 82 | raw_keystrokes: 'd2w', 83 | operator: 'd', 84 | motion: 'w', 85 | count: '2' 86 | }) 87 | assert_equal Operator.new({trigger:'d'}), operation.operator 88 | assert_equal BareMotion.new({motion: 'w', count: '2'}), operation.extent 89 | end 90 | 91 | it 'can be constructed from operator + operator' do 92 | operation = Operation.new({ 93 | raw_keystrokes: 'dd', 94 | operator: 'd', 95 | echo: 'd', 96 | }) 97 | assert_equal Operator.new({trigger: 'd'}), operation.operator 98 | assert_equal Echo.new({trigger: 'd'}), operation.extent 99 | end 100 | 101 | end 102 | 103 | describe VisualOperation do 104 | it '#selection is aware of the nature of the containing instance of VisualMode' do 105 | operation = VisualOperation.new 106 | commandlist = VisualMode.new 107 | commandlist << operation 108 | assert_equal 'selected characters', operation.selection 109 | commandlist.nature = 'linewise' 110 | assert_equal 'selected lines', operation.selection 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/vimprint/model/modes_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'vimprint/model/modes' 3 | 4 | class Element < Struct.new(:container); end 5 | 6 | module Vimprint 7 | 8 | describe BaseMode do 9 | 10 | it '#new sets element.container=self for each element' do 11 | one, two = [Element.new, Element.new] 12 | list = BaseMode.new(one, two) 13 | assert_equal [one, two], list.events 14 | assert_equal list, one.container 15 | assert_equal list, two.container 16 | end 17 | 18 | it '#<<(element) creates two-way reference between list and listitems' do 19 | (list = BaseMode.new) << (element = Element.new) 20 | assert_equal list, element.container 21 | end 22 | end 23 | 24 | describe VisualMode do 25 | 26 | it 'assumes charwise nature by default' do 27 | visual = VisualMode.new 28 | assert_equal visual.nature, "charwise" 29 | end 30 | 31 | it 'allows nature to be changed dynamically' do 32 | visual = VisualMode.new 33 | visual.nature = "linewise" 34 | assert_equal visual.nature, "linewise" 35 | end 36 | 37 | it 'allows nature to be set on creation' do 38 | one, two = [Element.new, Element.new] 39 | visual = VisualMode.new("linewise", one, two) 40 | assert_equal visual.nature, "linewise" 41 | # everything else works as it should...? 42 | assert_equal [one, two], visual.events 43 | assert_equal visual, one.container 44 | assert_equal visual, two.container 45 | end 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/vimprint/model/stage_test.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/autorun' 3 | require 'minitest/pride' 4 | require './lib/vimprint/model/stage' 5 | 6 | module Vimprint 7 | 8 | describe Stage do 9 | 10 | def test_reset_clears_state 11 | stage = Stage.new 12 | stage.add :register, '"a' 13 | stage.add :count, 4 14 | stage.add :trigger, 'x' 15 | stage.reset 16 | assert_equal({}, stage.to_hash) 17 | end 18 | 19 | def test_stage_to_hash 20 | stage = Stage.new 21 | stage.add :register, '"a' 22 | stage.add :count, 4 23 | stage.add :trigger, 'x' 24 | hashmap = { 25 | raw_keystrokes: '"a4x', 26 | count: 4, 27 | register: 'a', 28 | trigger: 'x', 29 | } 30 | assert_equal hashmap, stage.to_hash 31 | end 32 | 33 | def test_commit_returns_hash_and_clears_state 34 | stage = Stage.new 35 | stage.add :register, '"a' 36 | stage.add :count, 4 37 | stage.add :trigger, 'x' 38 | hashmap = { 39 | raw_keystrokes: '"a4x', 40 | count: 4, 41 | register: 'a', 42 | trigger: 'x', 43 | } 44 | assert_equal hashmap, stage.commit 45 | assert_equal({}, stage.to_hash) 46 | end 47 | 48 | def test_stage_accumulates_raw_keystrokes 49 | stage = Stage.new 50 | stage.add :register, '"a' 51 | stage.add :count, 4 52 | stage.add :trigger, 'x' 53 | assert_equal '"a4x', stage.raw_keystrokes 54 | end 55 | 56 | def test_counts_are_multiplied 57 | # Example: 58 | # given buffer contents: 123456789012345678901234567890 59 | # execute this command: 2"a3d4l 60 | # result: 61 | # all 3 counts are multiplied, (2*3*4=) 24 62 | # register a is set to: 123456789012345678901234 63 | stage = Stage.new 64 | stage.add :count, 2 65 | stage.add :register, '"a' 66 | stage.add :count, 3 67 | stage.add :operator, 'd' 68 | stage.add :count, 4 69 | stage.add :motion, 'l' 70 | assert_equal "a", stage.register 71 | assert_equal [2,3,4], stage.counts 72 | assert_equal '2"a3d4l', stage.raw_keystrokes 73 | assert_equal 24, stage.count 74 | end 75 | 76 | def test_register_is_overwritten 77 | # Example: 78 | # given buffer contents: 123456789 79 | # execute this command: 3"a2"bx 80 | # result: 81 | # both counts are multiplied, (3*2=) 6 82 | # register b is set to: 123456 83 | # register a is unchanged 84 | stage = Stage.new 85 | stage.add :count, 3 86 | stage.add :register, '"a' 87 | stage.add :count, 2 88 | stage.add :register, '"b' 89 | stage.add :trigger, 'x' 90 | assert_equal "b", stage.register 91 | assert_equal [3,2], stage.counts 92 | assert_equal '3"a2"bx', stage.raw_keystrokes 93 | end 94 | 95 | def test_staging_r_space 96 | stage = Stage.new 97 | stage.add :trigger, 'r' 98 | stage.add :printable_char, '' 99 | assert_equal({trigger: 'r', printable_char: '', raw_keystrokes: 'r'}, stage.to_hash) 100 | end 101 | 102 | def test_staging_operator_echo 103 | stage = Stage.new 104 | stage.add :operator, 'd' 105 | stage.add :operator, 'd' 106 | assert_equal({raw_keystrokes: 'dd', operator: 'd', echo: 'd'}, stage.to_hash) 107 | end 108 | 109 | def test_echo_is_true 110 | stage = Stage.new 111 | stage.add :operator, 'd' 112 | stage.add :operator, 'd' 113 | assert stage.echo_is_true? 114 | 115 | stage = Stage.new 116 | stage.add :operator, 'gU' 117 | stage.add :operator, 'gU' 118 | assert stage.echo_is_true? 119 | 120 | stage = Stage.new 121 | stage.add :operator, 'gU' 122 | stage.add :operator, 'U' 123 | assert stage.echo_is_true? 124 | end 125 | 126 | end 127 | 128 | end 129 | -------------------------------------------------------------------------------- /test/vimprint/ragel/parser_test.rb: -------------------------------------------------------------------------------- 1 | require 'vimprint/ragel/parser' 2 | 3 | module Vimprint 4 | 5 | describe Parser do 6 | 7 | def scan(keystrokes) 8 | Parser.new.process(keystrokes) 9 | end 10 | 11 | it 'accepts a simple "x" command' do 12 | assert_equal [NormalCommand.new({trigger: "x", count: nil, raw_keystrokes: "x"})], scan("x") 13 | end 14 | 15 | it 'accepts "2x" command' do 16 | assert_equal [NormalCommand.new({trigger: 'x', count: 2, raw_keystrokes: "2x"})], scan("2x") 17 | end 18 | 19 | describe '#keystrokes' do 20 | 21 | it 'returns simple characters unchanged' do 22 | assert_equal 'abcdef', Parser.new.keystrokes('abcdef') 23 | end 24 | 25 | it 'replaces " " with ' do 26 | assert_equal 'r', Parser.new.keystrokes('r ') 27 | end 28 | 29 | it 'replaces "\t" with ' do 30 | assert_equal 'r', Parser.new.keystrokes("r\t") 31 | end 32 | 33 | it 'replaces "\t" with ' do 34 | assert_equal '3', Parser.new.keystrokes("3\e") 35 | end 36 | 37 | end 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /test/vimprint/registry_test.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/autorun' 3 | require 'minitest/pride' 4 | require './lib/vimprint/registry' 5 | 6 | module Vimprint 7 | 8 | describe Explanation do 9 | 10 | it "can be a plain string" do 11 | explain = Explanation.new('undo the last change') 12 | assert_equal 'undo the last change', explain.render(binding) 13 | end 14 | 15 | it "can interpolate a local value into a string" do 16 | count = 5 17 | explain = Explanation.new(%q{undo the last #{count} changes}) 18 | assert_equal 'undo the last 5 changes', explain.render(binding) 19 | end 20 | 21 | it "can interpolate many local values into a string" do 22 | verb = 'move' 23 | count = 5 24 | explain = Explanation.new(%q{#{verb} to the start of the #{count.ordinalize} word}) 25 | assert_equal 'move to the start of the 5th word', explain.render(binding) 26 | end 27 | 28 | end 29 | 30 | describe Registry do 31 | 32 | describe "create_mode()" do 33 | 34 | it "doesn't create duplicates" do 35 | @normal_mode1 = Registry.create_mode("normal") 36 | @normal_mode2 = Registry.create_mode("normal") 37 | assert_same @normal_mode1, @normal_mode2 38 | end 39 | 40 | end 41 | 42 | describe "get_mode()" do 43 | 44 | before do 45 | @normal_mode = Registry.create_mode("normal") 46 | @insert_mode = Registry.create_mode("insert") 47 | end 48 | 49 | it 'returns the normal_mode registry' do 50 | mode = Registry.get_mode('normal') 51 | assert_equal @normal_mode, mode 52 | end 53 | 54 | it 'returns the insert_mode registry' do 55 | mode = Registry.get_mode('insert') 56 | assert_equal @insert_mode, mode 57 | end 58 | 59 | it 'explodes informatively when asked for a non-existant mode' do 60 | assert_raises(NoModeError) {Registry.get_mode('sparkles')} 61 | end 62 | 63 | end 64 | 65 | describe "#get_command" do 66 | 67 | before do 68 | @normal_mode = Registry.create_mode("normal") 69 | @normal_mode.create_command('w', 'move to the start of the next word') 70 | end 71 | 72 | it 'explains the specified command' do 73 | explained_motion = @normal_mode.get_command('w').template 74 | assert_equal "move to the start of the next word", explained_motion 75 | end 76 | 77 | it 'explodes informatively when asked for a non-existent command' do 78 | assert_raises(NoCommandError) {@normal_mode.get_command('sparkles')} 79 | end 80 | 81 | end 82 | 83 | describe "#create_command" do 84 | 85 | before do 86 | @normal_mode = Registry.create_mode("normal") 87 | end 88 | 89 | it 'instantiates an Explanation and saves it with a signature' do 90 | template = 'move to the end of the current word' 91 | @normal_mode.create_command('e', template) 92 | assert_equal template, @normal_mode.get_command('e').template 93 | end 94 | 95 | it 'overrides the existing explanation' do 96 | builtin, overridden = [ 'yank entire line', 'yank to end of line'] 97 | @normal_mode.create_command('Y', builtin) 98 | @normal_mode.create_command('Y', overridden) 99 | assert_equal overridden, @normal_mode.get_command('Y').template 100 | end 101 | end 102 | 103 | describe '#create_operator' do 104 | 105 | it 'overwrites existing entries' do 106 | @op1 = Registry.create_operator('d', 'delete') 107 | @op2 = Registry.create_operator('d', 'cut') 108 | assert_equal 'cut', Registry.get_operator('d') 109 | end 110 | 111 | end 112 | 113 | describe '#get_operator' do 114 | 115 | before do 116 | @cut = Registry.create_operator('d', 'cut') 117 | @rot13 = Registry.create_operator('g?', 'rot13') 118 | end 119 | 120 | it 'returns the cut registry' do 121 | mode = Registry.get_operator('d') 122 | assert_equal @cut, mode 123 | end 124 | 125 | it 'returns the rot13 registry' do 126 | mode = Registry.get_operator('g?') 127 | assert_equal @rot13, mode 128 | end 129 | 130 | it 'explodes informatively when asked for a non-existant mode' do 131 | assert_raises(NoOperatorError) {Registry.get_operator('sparkles')} 132 | end 133 | 134 | end 135 | 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /test/vimprint_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'vimprint' 3 | 4 | module Vimprint 5 | 6 | describe Vimprint do 7 | 8 | def ctrl_R 9 | #  - ascii: 18 10 | "\x12" # Ruby string notation for ctrl_R 11 | end 12 | 13 | def ctrl_V 14 | #  - ascii: 22 15 | "\x16" # Ruby string notation for ctrl_V 16 | end 17 | 18 | it 'prints consecutive commands with spaces to pad' do 19 | assert_equal 'x 2x "ax "zx 2"ax "a2x 3"a2x ', Vimprint.pp('x2x"ax"zx2"ax"a2x3"a2x') 20 | end 21 | 22 | it 'explains consecutive commands: \'x2x"ax"zx2"ax"a2x\'' do 23 | assert_equal [ 24 | ['x', 'cut character under cursor into default register'], 25 | ['2x', 'cut 2 characters into default register'], 26 | ['"ax', 'cut character under cursor into register a'], 27 | ['"zx', 'cut character under cursor into register z'], 28 | ['2"ax', 'cut 2 characters into register a'], 29 | ['"a2x', 'cut 2 characters into register a'], 30 | ['3"a2x', 'cut 6 characters into register a'] 31 | ], Vimprint.explain("x2x\"ax\"zx2\"ax\"a2x3\"a2x") 32 | end 33 | 34 | it 'explains both x and X commands' do 35 | assert_equal [ 36 | ['x', 'cut character under cursor into default register'], 37 | ['X', 'cut 1 character before cursor into default register'], 38 | ['2X', 'cut 2 characters before cursor into default register'], 39 | ['"aX', 'cut 1 character before cursor into register a'], 40 | ['"z3X', 'cut 3 characters before cursor into register z'], 41 | ], Vimprint.explain('xX2X"aX"z3X') 42 | end 43 | 44 | it 'explains distroke commands' do 45 | assert_equal [ 46 | ['ma', 'save current position with local mark a'], 47 | ['mZ', 'save current position with global mark Z'], 48 | ['`a', 'jump to local mark a'], 49 | ['2`Z', 'jump to global mark Z'], 50 | ], Vimprint.explain('mamZ`a2`Z') 51 | end 52 | 53 | it 'explains undo and redo commands' do 54 | assert_equal [ 55 | ['u', 'undo 1 change'], 56 | ['2u', 'undo 2 changes'], 57 | ["3", 'redo 3 changes'], 58 | ], Vimprint.explain("u2u3#{ctrl_R}") 59 | end 60 | 61 | it 'explains the r command' do 62 | assert_equal [ 63 | ['ra', 'replace current character with a'], 64 | ['rZ', 'replace current character with Z'], 65 | ['r', 'replace current character with '], 66 | ['r', 'replace current character with '], 67 | ['r', 'replace current character with '], 68 | ['3rx', 'replace next 3 characters with x'], 69 | ], Vimprint.explain("rarZr r\tr\r3rx") 70 | end 71 | 72 | it 'explains aborted commands' do 73 | explanation = '[aborted command]' 74 | bad_input = [ 75 | "3\e", 76 | "\"a\e", 77 | "2\"x3\e", 78 | "r\e", 79 | "m\e", 80 | "3m\e", 81 | "`\e", 82 | "d\e", 83 | "d\t", 84 | "d\"", 85 | ].join 86 | 87 | assert_equal [ 88 | ['3', explanation], 89 | ['"a', explanation], 90 | ['2"x3', explanation], 91 | ['r', explanation], 92 | ['m', explanation], 93 | ['3m', explanation], 94 | ['`', explanation], 95 | ['d', explanation], 96 | ['d', explanation], 97 | ['d"', explanation], 98 | ], Vimprint.explain(bad_input) 99 | end 100 | 101 | it 'explains combinations of counts and registers' do 102 | weird_input = [ 103 | '"a"b"cx', 104 | '2"a3"b4"cx', 105 | '"a3"b4"cx', 106 | '"a3"b4"cma', 107 | '"a3"b4"cu', 108 | '"a3"b4"crx', 109 | ].join 110 | 111 | assert_equal [ 112 | ['"a"b"cx', 'cut character under cursor into register c'], 113 | ['2"a3"b4"cx', 'cut 24 characters into register c'], 114 | ['"a3"b4"cx', 'cut 12 characters into register c'], 115 | ['"a3"b4"cma', 'save current position with local mark a'], 116 | ['"a3"b4"cu', 'undo 12 changes'], 117 | ['"a3"b4"crx', 'replace next 12 characters with x'], 118 | ], Vimprint.explain(weird_input) 119 | end 120 | 121 | it 'explains motions used in Normal mode' do 122 | assert_equal [ 123 | ['w', 'move forward to start of next word'], 124 | ['3w', 'move forward to start of 3rd word'], 125 | ['e', 'move forward to end of word'], 126 | ['2e', 'move forward to end of 2nd word'], 127 | ], Vimprint.explain('w3we2e') 128 | end 129 | 130 | it 'explains motions used after an operator' do 131 | assert_equal [ 132 | ['dw', 'delete to start of next word'], 133 | ['2dw', 'delete to start of 2nd word'], 134 | ['d3w', 'delete to start of 3rd word'], 135 | ], Vimprint.explain('dw2dwd3w') 136 | end 137 | 138 | it 'explains linewise operations' do 139 | commands = %w{dd 2>> g?3g? gUU 3d2d}.join 140 | assert_equal [ 141 | ['dd', 'delete a line'], 142 | ['2>>', 'indent 2 lines'], 143 | ['g?3g?', 'rot13 encode 3 lines'], 144 | ['gUU', 'upcase a line'], 145 | ['3d2d', 'delete 6 lines'], 146 | ], Vimprint.explain(commands) 147 | end 148 | 149 | it 'aborts operator pending mode on encountering non-self operator' do 150 | bad_input = [ 151 | "d>", 152 | ">d", 153 | ].join 154 | assert_equal [ 155 | ['d>', '[aborted command]'], 156 | ['>d', '[aborted command]'], 157 | ], Vimprint.explain(bad_input) 158 | end 159 | 160 | # consider the following: 161 | # de - cut from cursor forward to end of word 162 | # dw - cut from cursor forward to start of next word 163 | # ce - cut from cursor forward to end of word 164 | # cw - cut from cursor forward to end of word 165 | # 166 | # de - cut from cursor forward to end of word, save text to default register 167 | # dw - cut from cursor forward to start of next word, save text to default register 168 | # ce - cut from cursor forward to end of word, save text to default register, start Insert mode 169 | # cw - cut from cursor forward to end of word, save text to default register, start Insert mode 170 | # 171 | # cw breaks from convention! 172 | # 173 | # Also, think about how to explain all of the command! 174 | # de - from cursor to end of word, cut text into default register 175 | # dw - from cursor to start of next word, cut text into default register 176 | # ce - from cursor to end of word, cut text into default register and start Insert mode 177 | # cw - from cursor to end of word, cut text into default register and start Insert mode 178 | # 179 | # Also, think about different descriptions for charwise/linewise motions 180 | # 2dw - cut from cursor forward to start of 2nd word 181 | # dd - cut 1 line 182 | # 2dd - cut 2 lines 183 | # dj - cut this line and 1 below 184 | # 2dj - cut this line and 2 below 185 | # dj - cut 2 lines 186 | # 2dj - cut 3 lines 187 | # dvj - cut from cursor same position on line below 188 | 189 | it 'detects Visual mode start and exit' do 190 | keystrokes = [ 191 | "v\e", 192 | "V\e", 193 | "#{ctrl_V}\e", 194 | "vv", 195 | "VV", 196 | "#{ctrl_V}#{ctrl_V}", 197 | ].join 198 | assert_equal [ 199 | ["v", "start Visual mode charwise"], 200 | ["", "pop to Normal mode"], 201 | ["V", "start Visual mode linewise"], 202 | ["", "pop to Normal mode"], 203 | ["", "start Visual mode blockwise"], 204 | ["", "pop to Normal mode"], 205 | ["v", "start Visual mode charwise"], 206 | ["v", "pop to Normal mode"], 207 | ["V", "start Visual mode linewise"], 208 | ["V", "pop to Normal mode"], 209 | ["", "start Visual mode blockwise"], 210 | ["", "pop to Normal mode"], 211 | ], Vimprint.explain(keystrokes) 212 | end 213 | 214 | it 'can switch between different Visual modes' do 215 | keystrokes = [ 216 | "vV#{ctrl_V}v#{ctrl_V}Vv\e", 217 | ].join 218 | assert_equal [ 219 | ["v", "start Visual mode charwise"], 220 | ["V", "change to Visual mode linewise"], 221 | ["", "change to Visual mode blockwise"], 222 | ["v", "change to Visual mode charwise"], 223 | ["", "change to Visual mode blockwise"], 224 | ["V", "change to Visual mode linewise"], 225 | ["v", "change to Visual mode charwise"], 226 | ["", "pop to Normal mode"], 227 | # ["", "beep!"], 228 | ], Vimprint.explain(keystrokes) 229 | end 230 | 231 | it 'starts up lastwise visual mode with gv' do 232 | keystrokes = [ 233 | "v\egvv", 234 | "V\egvV", 235 | "#{ctrl_V}\egv#{ctrl_V}", 236 | ].join 237 | assert_equal [ 238 | ["v", "start Visual mode charwise"], 239 | ["", "pop to Normal mode"], 240 | ["gv", "start Visual mode and reselect previous selection"], 241 | ["v", "pop to Normal mode"], 242 | ["V", "start Visual mode linewise"], 243 | ["", "pop to Normal mode"], 244 | ["gv", "start Visual mode and reselect previous selection"], 245 | ["V", "pop to Normal mode"], 246 | ["", "start Visual mode blockwise"], 247 | ["", "pop to Normal mode"], 248 | ["gv", "start Visual mode and reselect previous selection"], 249 | ["", "pop to Normal mode"], 250 | ], Vimprint.explain(keystrokes) 251 | end 252 | 253 | it 'explains motions used in Visual mode' do 254 | assert_equal [ 255 | ["v", "start Visual mode charwise"], 256 | ['w', 'select to start of next word'], 257 | ['2w', 'select to start of 2nd word'], 258 | ], Vimprint.explain('vw2w') 259 | end 260 | 261 | it 'explains visual operations' do 262 | assert_equal [ 263 | ["v", "start Visual mode charwise"], 264 | ["d", "delete selected characters"], 265 | ["V", "start Visual mode linewise"], 266 | ["d", "delete selected lines"], 267 | ["V", "start Visual mode linewise"], 268 | ["U", "upcase selected lines"], 269 | ["v", "start Visual mode charwise"], 270 | ["u", "downcase selected characters"], 271 | ["v", "start Visual mode charwise"], 272 | [">", "indent selection"], 273 | ], Vimprint.explain('vdVdVUvuv>') 274 | end 275 | 276 | end 277 | 278 | end 279 | -------------------------------------------------------------------------------- /vimprint.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "vimprint/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "vimprint" 7 | s.version = Vimprint::VERSION 8 | s.authors = ["Drew Neil"] 9 | s.email = ["andrew.jr.neil@gmail.com"] 10 | s.homepage = "" 11 | s.summary = %q{Parse Vim keystrokes, pretty print them.} 12 | s.description = %q{vimprint takes a stream of Vim keystrokes as input, and transforms them into something more easy on the eye. } 13 | 14 | s.rubyforge_project = "vimprint" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | # specify any dependencies here: 22 | s.add_development_dependency "rake" 23 | s.add_development_dependency "minitest" 24 | s.add_runtime_dependency "nokogiri", "~> 1.5" 25 | end 26 | --------------------------------------------------------------------------------