├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── _project.vim ├── bin └── vimrunner ├── lib ├── vimrunner.rb └── vimrunner │ ├── client.rb │ ├── command.rb │ ├── errors.rb │ ├── path.rb │ ├── platform.rb │ ├── rspec.rb │ ├── server.rb │ ├── testing.rb │ └── version.rb ├── spec ├── spec_helper.rb ├── vimrunner │ ├── client_spec.rb │ ├── command_spec.rb │ ├── errors_spec.rb │ ├── path_spec.rb │ ├── platform_spec.rb │ ├── server_spec.rb │ └── testing_spec.rb └── vimrunner_spec.rb ├── vim ├── vimrc └── vimrunner.vim └── vimrunner.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | tags 3 | pkg/* 4 | doc/* 5 | tmp/* 6 | coverage/* 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format documentation 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | dist: trusty 3 | sudo: false 4 | cache: bundler 5 | rvm: 6 | - 2.4.1 7 | addons: 8 | apt: 9 | packages: 10 | - vim-gtk 11 | env: 12 | - TRAVIS_CI=1 13 | before_script: 14 | - "export DISPLAY=:99.0" 15 | - "sh -e /etc/init.d/xvfb start" 16 | - "vim --version" 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vimrunner [![Build Status](https://secure.travis-ci.org/AndrewRadev/vimrunner.svg?branch=master)](http://travis-ci.org/AndrewRadev/vimrunner) 2 | 3 | Using Vim's 4 | [client/server](http://vimdoc.sourceforge.net/htmldoc/remote.html#clientserver) 5 | functionality, this library exposes a way to spawn a Vim instance and control 6 | it programatically. Apart from being a fun party trick, this can be used to do 7 | integration testing on Vimscript. 8 | 9 | ![Demo](http://i.andrewradev.com/cb035fee68a149c09c3a252fed91b177.gif) 10 | 11 | The latest stable documentation can be found 12 | [on rubydoc.info](http://rubydoc.info/gems/vimrunner/frames). 13 | 14 | Any issue reports or contributions are very welcome on the 15 | [GitHub issue tracker](https://github.com/AndrewRadev/Vimrunner/issues). 16 | 17 | ## Usage 18 | 19 | If you don't already have a running Vim server, Vimrunner can be used in one 20 | of two main ways: 21 | 22 | ```ruby 23 | # Vim will automatically be started and killed. 24 | Vimrunner.start do |vim| 25 | vim.edit "file.txt" 26 | vim.insert "Hello" 27 | vim.write 28 | end 29 | ``` 30 | 31 | ```ruby 32 | # Vim will automatically be started but you must manually kill it when you are 33 | # finished. 34 | vim = Vimrunner.start 35 | vim.edit "file.txt" 36 | vim.insert "Hello" 37 | vim.write 38 | vim.kill 39 | ``` 40 | 41 | Vimrunner will attempt to start up the most suitable version of Vim available, 42 | meaning one of the following: 43 | 44 | * `vim` if it supports headlessly creating servers (see [Requirements](#requirements) below for more information); 45 | * `mvim` if you are on Mac OS X; 46 | * `gvim`. 47 | 48 | If you wish to always start a GUI Vim (viz. skip using a headless `vim`) then 49 | you can use `start_gvim` like so: 50 | 51 | ```ruby 52 | Vimrunner.start_gvim do |vim| 53 | # ... 54 | end 55 | ``` 56 | 57 | If you require an even more specific version of Vim, you can pass the path to 58 | it by instantiating your own `Server` instance like so: 59 | 60 | ```ruby 61 | Vimrunner::Server.new(:executable => "/path/to/my/specific/vim").start do |vim| 62 | vim.edit "file.txt" 63 | end 64 | ``` 65 | 66 | (You can also use the non-block form of `start` in both of the above 67 | examples.) 68 | 69 | Calling `start` (or `start_gvim`) will return a `Client` instance with which 70 | you can control Vim. For a full list of methods you can invoke on the remote 71 | Vim instance, check out the `Client` 72 | [documentation](http://rubydoc.info/gems/vimrunner/Vimrunner/Client). 73 | 74 | If you already have a remote-capable Vim server running, you can connect 75 | Vimrunner to it directly by using `Vimrunner.connect` or `Vimrunner.connect!` 76 | like so: 77 | 78 | ```ruby 79 | # Assuming a running Vim server called FOO... 80 | vim = Vimrunner.connect("FOO") 81 | if vim 82 | vim.insert("Hello world!") 83 | end 84 | 85 | # Or, if you're confident there's a running server... 86 | vim = Vimrunner.connect!("FOO") 87 | vim.insert("Hello world!") 88 | ``` 89 | 90 | In case of failure to find the server `FOO`, the first form will return `nil`, 91 | while the second form will raise an exception. 92 | 93 | ## Testing 94 | 95 | If you're using Vimrunner for testing vim plugins, a simple way to get up and 96 | running is by requiring the `vimrunner/rspec` file. With that, your 97 | `spec_helper.rb` would look like this: 98 | 99 | ``` ruby 100 | require 'vimrunner' 101 | require 'vimrunner/rspec' 102 | 103 | Vimrunner::RSpec.configure do |config| 104 | # Use a single Vim instance for the test suite. Set to false to use an 105 | # instance per test (slower, but can be easier to manage). 106 | config.reuse_server = true 107 | 108 | # Decide how to start a Vim instance. In this block, an instance should be 109 | # spawned and set up with anything project-specific. 110 | config.start_vim do 111 | vim = Vimrunner.start 112 | 113 | # Or, start a GUI instance: 114 | # vim = Vimrunner.start_gvim 115 | 116 | # Setup your plugin in the Vim instance 117 | plugin_path = File.expand_path('../..', __FILE__) 118 | vim.add_plugin(plugin_path, 'plugin/my_plugin.vim') 119 | 120 | # The returned value is the Client available in the tests. 121 | vim 122 | end 123 | end 124 | ``` 125 | 126 | This will result in: 127 | 128 | - A `vim` helper in every rspec example, returning the configured 129 | `Vimrunner::Client` instance. 130 | - Every example is executed in a separate temporary directory to make it easier 131 | to manipulate files. 132 | - A few helper methods from the `Vimrunner::Testing` module 133 | ([documentation](http://rubydoc.info/gems/vimrunner/Vimrunner/Testing)). 134 | 135 | The specs would then look something like this: 136 | 137 | ``` ruby 138 | require 'spec_helper' 139 | 140 | describe "My Vim plugin" do 141 | specify "some behaviour" do 142 | write_file('test.rb', <<-EOF) 143 | def foo 144 | bar 145 | end 146 | EOF 147 | 148 | vim.edit 'test.rb' 149 | do_plugin_related_stuff_with(vim) 150 | vim.write 151 | 152 | IO.read('test.rb').should eq normalize_string_indent(<<-EOF) 153 | def bar 154 | foo 155 | end 156 | EOF 157 | end 158 | end 159 | ``` 160 | 161 | If you need a different setup, please look through the file 162 | `lib/vimrunner/rspec.rb` for ideas on how to build your own testing scaffold. 163 | 164 | ## Requirements 165 | 166 | Vim needs to be compiled with `+clientserver`. This should be available with 167 | the `normal`, `big` and `huge` featuresets or by using 168 | [MacVim](http://code.google.com/p/macvim/) on Mac OS X. In order to start a 169 | server without a GUI, you will also need `+xterm-clipboard` [as described in 170 | the Vim 171 | manual](http://vimdoc.sourceforge.net/htmldoc/remote.html#x11-clientserver). 172 | 173 | The client/server functionality (regrettably) needs a running X server to 174 | function, even without a GUI. This means that if you're using it for 175 | automated tests on a remote server, you'll probably need to start it with 176 | Xvfb. 177 | 178 | If you are using MacVim, note that you will need the `mvim` binary in your 179 | `PATH` in order to start and communicate with Vim servers. 180 | 181 | ## Experimenting 182 | 183 | The `vimrunner` executable opens up an irb session with `$vim` set to a running 184 | `gvim` (or `mvim`) client. You can use this for interactive experimentation. A 185 | few things you can try: 186 | 187 | ``` ruby 188 | $vim.edit 'some_file_name' # edit a file 189 | $vim.insert 'Hello, World!' # enter insert mode and write some text 190 | $vim.normal 'T,' # go back to the nearest comma 191 | $vim.type 'a' # append a newline after the comma 192 | $vim.write # write file to disk 193 | ``` 194 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rspec/core/rake_task' 3 | 4 | Bundler::GemHelper.install_tasks 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | desc "Generate documentation" 8 | task :doc do 9 | sh 'rdoc -t "Vimrunner" lib' 10 | end 11 | 12 | task :default => :spec 13 | -------------------------------------------------------------------------------- /_project.vim: -------------------------------------------------------------------------------- 1 | runtime projects/ruby.vim 2 | -------------------------------------------------------------------------------- /bin/vimrunner: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | $: << File.expand_path('../../lib', __FILE__) 4 | 5 | require 'irb' 6 | require 'vimrunner' 7 | 8 | $vim = Vimrunner.start_gvim 9 | 10 | IRB.start 11 | -------------------------------------------------------------------------------- /lib/vimrunner.rb: -------------------------------------------------------------------------------- 1 | require "vimrunner/server" 2 | require "vimrunner/platform" 3 | 4 | module Vimrunner 5 | 6 | # Public: Start a Vim process and return a Client through which it can be 7 | # controlled. 8 | # 9 | # vim - The String path to the Vim you wish to use (default: the most 10 | # appropriate Vim for your system). 11 | # blk - An optional block which will be passed a Client with which you can 12 | # communicate with the Vim process. Upon exiting the block, the Vim 13 | # process will be terminated. 14 | # 15 | # Examples 16 | # 17 | # client = Vimrunner.start 18 | # # => # 19 | # 20 | # Vimrunner.start do |client| 21 | # client.command("version") 22 | # end 23 | # 24 | # Returns a Client for the started Server. 25 | def self.start(vim = Platform.vim, &blk) 26 | Server.new(:executable => vim).start(&blk) 27 | end 28 | 29 | # Public: Start a Vim process with a GUI and return a Client through which 30 | # it can be controlled. 31 | # 32 | # vim - The String path to the Vim you wish to use (default: the most 33 | # appropriate Vim for your system). 34 | # blk - An optional block which will be passed a Client with which you can 35 | # communicate with the Vim process. Upon exiting the block, the Vim 36 | # process will be terminated. 37 | # 38 | # Examples 39 | # 40 | # client = Vimrunner.start 41 | # # => # 42 | # 43 | # Vimrunner.start do |client| 44 | # client.command("version") 45 | # end 46 | # 47 | # Returns a Client for the started Server. 48 | def self.start_gvim(&blk) 49 | Server.new(:executable => Platform.gvim).start(&blk) 50 | end 51 | 52 | # Public: Connect to an existing Vim process by name. Returns nil in case of 53 | # failure. 54 | # 55 | # name - The String name of the Vim server to connect to. 56 | # 57 | # Examples 58 | # 59 | # client = Vimrunner.connect("FOO") 60 | # # => # 61 | # 62 | # Returns a Client for the named server. 63 | def self.connect(name) 64 | Server.new(:name => name).connect 65 | end 66 | 67 | # Public: Connect to an existing Vim process by name. Raises an exception in 68 | # case of failure. 69 | # 70 | # name - The String name of the Vim server to connect to. 71 | # 72 | # Examples 73 | # 74 | # client = Vimrunner.connect("FOO") 75 | # # => # 76 | # 77 | # Returns a Client for the named server. 78 | def self.connect!(name) 79 | Server.new(:name => name).connect! 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/vimrunner/client.rb: -------------------------------------------------------------------------------- 1 | require "vimrunner/path" 2 | require "vimrunner/command" 3 | 4 | module Vimrunner 5 | class Client 6 | attr_reader :server 7 | 8 | def initialize(server) 9 | @server = server 10 | end 11 | 12 | # Public: Adds a plugin to Vim's runtime. Initially, Vim is started 13 | # without sourcing any plugins to ensure a clean state. This method can be 14 | # used to populate the instance's environment. 15 | # 16 | # dir - The base directory of the plugin, the one that contains 17 | # its autoload, plugin, ftplugin, etc. directories. 18 | # entry_script - The Vim script that's runtime'd to initialize the plugin 19 | # (optional). 20 | # 21 | # Examples 22 | # 23 | # vim.add_plugin 'rails', 'plugin/rails.vim' 24 | # 25 | # Returns nothing. 26 | def add_plugin(dir, entry_script = nil) 27 | append_runtimepath(dir) 28 | if entry_script 29 | entry_script_path = Path.new(entry_script) 30 | command("runtime #{entry_script_path}") 31 | end 32 | end 33 | 34 | # Public: source a script in Vim server 35 | # 36 | # script - The Vim script to be sourced 37 | # 38 | # Examples 39 | # 40 | # vim.source '/path/to/plugin/rails.vim' 41 | # 42 | # Returns nothing. 43 | def source(script) 44 | script_path = Path.new(script) 45 | feedkeys(":\\source #{script_path}\\") 46 | end 47 | 48 | # Public: Appends a directory to Vim's runtimepath 49 | # 50 | # dir - The directory added to the path 51 | # 52 | # Returns nothing. 53 | def append_runtimepath(dir) 54 | dir_path = Path.new(dir) 55 | command("set runtimepath+=#{dir_path}") 56 | end 57 | 58 | # Public: Prepends a directory to Vim's runtimepath. Use this instead of 59 | # #append_runtimepath to give the directory higher priority when Vim 60 | # runtime's a file. 61 | # 62 | # dir - The directory added to the path 63 | # 64 | # Returns nothing. 65 | def prepend_runtimepath(dir) 66 | dir_path = Path.new(dir) 67 | runtimepath = Path.new(echo('&runtimepath')) 68 | command("set runtimepath=#{dir_path},#{runtimepath}") 69 | end 70 | 71 | # Public: Switches Vim to normal mode and types in the given keys. 72 | # 73 | # Returns the Client instance. 74 | def normal(keys = "") 75 | server.remote_send("#{keys}") 76 | self 77 | end 78 | 79 | # Public: Invokes one of the basic actions the Vim server supports, 80 | # sending a key sequence. The keys are sent as-is, so it'd probably be 81 | # better to use the wrapper methods, #normal, #insert and so on. 82 | # 83 | # Returns the Client instance. 84 | def type(keys) 85 | server.remote_send(keys) 86 | self 87 | end 88 | 89 | # Public: Starts a search in Vim for the given text. The result is that 90 | # the cursor is positioned on its first occurrence. 91 | # 92 | # Returns the Client instance. 93 | def search(text) 94 | normal 95 | type "/#{text}" 96 | end 97 | 98 | # Public: Switches Vim to insert mode and types in the given text. 99 | # 100 | # Returns the Client instance. 101 | def insert(text) 102 | normal "i#{text}" 103 | end 104 | 105 | # Public: Writes the file being edited to disk. Note that you probably 106 | # want to set the file's name first by using Runner#edit. 107 | # 108 | # Returns the Client instance. 109 | def write 110 | command :write 111 | self 112 | end 113 | 114 | # Public: Echo each expression with a space in between. 115 | # 116 | # Returns the String output. 117 | def echo(*expressions) 118 | command "echo #{expressions.join(' ')}" 119 | end 120 | 121 | # Public: Send keys as if they come from a mapping or typed by a user. 122 | # 123 | # Vim's usual remote-send functionality to send keys to a server does not 124 | # respect mappings. As a workaround, the feedkeys() function can be used 125 | # to more closely simulate user input. 126 | # 127 | # Any keys are sent in a double-quoted string so that special keys such as 128 | # and can be used. Note that, as per Vim documentation, such 129 | # keys should be preceded by a backslash, e.g. '\' for a carriage 130 | # return, '' will send those four characters separately. 131 | # 132 | # Examples 133 | # 134 | # vim.command 'map ihello' 135 | # vim.feedkeys '\' 136 | # 137 | # Returns nothing. 138 | def feedkeys(string) 139 | string = string.gsub('"', '\"') 140 | server.remote_expr(%Q{feedkeys("#{string}")}) 141 | end 142 | 143 | # Public: Sets a setting in Vim. If +value+ is nil, the setting is 144 | # considered to be a boolean. 145 | # 146 | # Examples 147 | # 148 | # vim.set 'expandtab' # invokes ":set expandtab" 149 | # vim.set 'tabstop', 3 # invokes ":set tabstop=3" 150 | # 151 | # Returns the Client instance 152 | def set(setting, value = nil) 153 | if value 154 | command "set #{setting}=#{value}" 155 | else 156 | command "set #{setting}" 157 | end 158 | self 159 | end 160 | 161 | # Public: Edits the file +filename+ with Vim. 162 | # 163 | # Note that this doesn't use the '--remote' Vim flag, it simply types in 164 | # the command manually. This is necessary to avoid the Vim instance 165 | # getting focus. 166 | # 167 | # Returns the Client instance. 168 | def edit(filename) 169 | file_path = Path.new(filename) 170 | command "edit #{file_path}" 171 | self 172 | end 173 | 174 | # Public: Edits the file +filename+ with Vim using edit!. 175 | # 176 | # Similar to #edit, only discards any changes to the current buffer. 177 | # 178 | # Returns the Client instance. 179 | def edit!(filename) 180 | file_path = Path.new(filename) 181 | command "edit! #{file_path}" 182 | self 183 | end 184 | 185 | # Public: Executes the given command in the Vim instance and returns its 186 | # output, stripping all surrounding whitespace. 187 | # 188 | # Returns the String output. 189 | # Raises InvalidCommandError if the command is not recognised by vim. 190 | def command(commands) 191 | command = Command.new(commands) 192 | server.remote_expr("VimrunnerEvaluateCommandOutput('#{command}')").tap do |output| 193 | raise InvalidCommandError.new(output) if output =~ /^Vim(\(call\))?:E\d+:/ 194 | end 195 | end 196 | 197 | # Kills the server it's connected to. 198 | def kill 199 | server.kill 200 | end 201 | 202 | # Bring the server to foreground 203 | def foreground 204 | server.remote_expr("foreground()") 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/vimrunner/command.rb: -------------------------------------------------------------------------------- 1 | module Vimrunner 2 | class Command 3 | def initialize(command) 4 | @command = command 5 | end 6 | 7 | def to_s 8 | @command.to_s.gsub("'", "''") 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/vimrunner/errors.rb: -------------------------------------------------------------------------------- 1 | module Vimrunner 2 | class InvalidCommandError < RuntimeError; end 3 | class ExecutionError < RuntimeError; end 4 | 5 | class NoSuitableVimError < RuntimeError 6 | def message 7 | "No suitable Vim executable could be found for this system." 8 | end 9 | end 10 | 11 | class TimeoutError < RuntimeError 12 | def message 13 | "Timed out while waiting for serverlist. Is an X11 server running?" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/vimrunner/path.rb: -------------------------------------------------------------------------------- 1 | module Vimrunner 2 | class Path 3 | def initialize(path) 4 | @path = path 5 | end 6 | 7 | def to_s 8 | @path.to_s.gsub(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/vimrunner/platform.rb: -------------------------------------------------------------------------------- 1 | require "rbconfig" 2 | 3 | require "vimrunner/errors" 4 | 5 | module Vimrunner 6 | 7 | # Public: The Platform module contains logic for finding a Vim executable 8 | # that supports the clientserver functionality on the current system. Its 9 | # methods can be used to fetch a Vim path for initializing a Server. 10 | # 11 | # Examples 12 | # 13 | # Vimrunner::Platform.vim 14 | # # => "gvim" 15 | module Platform 16 | extend self 17 | 18 | # Public: Looks for a Vim executable that's suitable for the current 19 | # platform. Also attempts to find an appropriate GUI vim if terminal ones 20 | # are unsuitable. 21 | # 22 | # Returns the String Vim executable. 23 | # Raises NoSuitableVimError if no suitable Vim can be found. 24 | def vim 25 | vims.find { |vim| suitable?(vim) } or raise NoSuitableVimError 26 | end 27 | 28 | # Public: Looks for a GUI Vim executable that's suitable for the current 29 | # platform. 30 | # 31 | # Returns the String Vim executable. 32 | # Raises NoSuitableVimError if no suitable Vim can be found. 33 | def gvim 34 | gvims.find { |gvim| suitable?(gvim) } or raise NoSuitableVimError 35 | end 36 | 37 | private 38 | 39 | def gvims 40 | if mac? 41 | %w( mvim gvim ) 42 | else 43 | %w( gvim ) 44 | end 45 | end 46 | 47 | def vims 48 | %w( vim ) + gvims 49 | end 50 | 51 | def suitable?(vim) 52 | features = features(vim) 53 | 54 | if gui?(vim) 55 | features.include?("+clientserver") 56 | else 57 | features.include?("+clientserver") && features.include?("+xterm_clipboard") 58 | end 59 | end 60 | 61 | def gui?(vim) 62 | executable = File.basename(vim) 63 | 64 | executable[0, 1] == "g" || executable[0, 1] == "m" 65 | end 66 | 67 | def features(vim) 68 | IO.popen([vim, "--version"]) { |io| io.read.strip } 69 | rescue Errno::ENOENT 70 | "" 71 | end 72 | 73 | def mac? 74 | RbConfig::CONFIG["host_os"] =~ /darwin/ 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/vimrunner/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'vimrunner' 2 | require 'vimrunner/testing' 3 | 4 | module Vimrunner 5 | module RSpec 6 | class Configuration 7 | attr_accessor :reuse_server 8 | 9 | def start_vim(&block) 10 | @start_vim_method = block 11 | end 12 | 13 | def start_vim_method 14 | @start_vim_method || lambda { Vimrunner.start } 15 | end 16 | end 17 | 18 | def self.configuration 19 | @configuration ||= Configuration.new 20 | end 21 | 22 | def self.configure 23 | yield configuration 24 | end 25 | end 26 | 27 | module Testing 28 | class << self 29 | attr_accessor :instance 30 | end 31 | 32 | def vim 33 | Testing.instance ||= Vimrunner::RSpec.configuration.start_vim_method.call 34 | end 35 | end 36 | end 37 | 38 | # Default configuration 39 | Vimrunner::RSpec.configure do |config| 40 | config.reuse_server = false 41 | end 42 | 43 | RSpec.configure do |config| 44 | 45 | # Include the Testing DSL into all examples. 46 | config.include(Vimrunner::Testing) 47 | 48 | # Each example is executed in a separate directory 49 | config.around(:each) do |example| 50 | Dir.mktmpdir do |dir| 51 | Dir.chdir(dir) do 52 | vim.command("cd #{dir}") 53 | example.run 54 | end 55 | end 56 | end 57 | 58 | config.before(:each) do 59 | unless Vimrunner::RSpec.configuration.reuse_server 60 | Vimrunner::Testing.instance.kill if Vimrunner::Testing.instance 61 | Vimrunner::Testing.instance = nil 62 | end 63 | end 64 | 65 | # Kill the Vim server after all tests are over. 66 | config.after(:suite) do 67 | Vimrunner::Testing.instance.kill if Vimrunner::Testing.instance 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/vimrunner/server.rb: -------------------------------------------------------------------------------- 1 | require "timeout" 2 | require "pty" 3 | require "open3" 4 | 5 | require "vimrunner/errors" 6 | require "vimrunner/client" 7 | require "vimrunner/platform" 8 | 9 | module Vimrunner 10 | 11 | # Public: A Server has the responsibility of starting a Vim process and 12 | # communicating with it through the clientserver interface. The process can 13 | # be started with "start" and stopped with "kill". If given the servername of 14 | # an existing Vim instance, it can control that instance without the need to 15 | # start a new process. 16 | # 17 | # A Client would be necessary as an actual interface, though it is possible 18 | # to use a Server directly to invoke --remote commands on its Vim instance. 19 | class Server 20 | VIMRC = File.expand_path("../../../vim/vimrc", __FILE__) 21 | VIMRUNNER_RC = File.expand_path("../../../vim/vimrunner.vim", __FILE__) 22 | 23 | attr_reader :name, :executable, :vimrc, :gvimrc, :pid 24 | 25 | # Public: Initialize a Server 26 | # 27 | # options - The Hash options used to define a server (default: {}): 28 | # :executable - The String Vim executable to use (optional) 29 | # (default: Platform.vim). 30 | # :name - The String name of the Vim server (optional) 31 | # (default: "VIMRUNNER#{rand}"). 32 | # :vimrc - The String vimrc file to source in the client (optional) 33 | # (default: Server::VIMRC). 34 | # :foreground - Boolean, whether to start Vim with the -f option (optional) 35 | # (default: true). 36 | # 37 | def initialize(options = {}) 38 | @executable = options.fetch(:executable) { Platform.vim } 39 | @name = options.fetch(:name) { "VIMRUNNER#{rand}" }.upcase 40 | @vimrc = options.fetch(:vimrc) { VIMRC } 41 | @gvimrc = options.fetch(:gvimrc) { "NONE" } 42 | @foreground = options.fetch(:foreground, true) 43 | end 44 | 45 | # Public: Start a Server. This spawns a background process. 46 | # 47 | # Examples 48 | # 49 | # client = Vimrunner::Server.new("vim").start 50 | # # => # 51 | # 52 | # Vimrunner::Server.new("vim").start do |client| 53 | # client.edit("foo") 54 | # end 55 | # 56 | # Returns a new Client instance initialized with this Server. 57 | # Yields a new Client instance initialized with this Server. 58 | def start 59 | @r, @w, @pid = spawn 60 | 61 | if block_given? 62 | begin 63 | @result = yield(connect!) 64 | ensure 65 | kill 66 | end 67 | @result 68 | else 69 | connect! 70 | end 71 | end 72 | 73 | # Public: Connects to the running server by name, blocking if need be. 74 | # Returns nil if no server was found in the given time. 75 | # 76 | # options - An optional Hash. For now, only used for specifying a timeout 77 | # (default: {}): 78 | # 79 | # :timeout - The Integer timeout to wait for a running server (optional) 80 | # (default: 5). 81 | # 82 | # Returns a new Client instance initialized with this Server. 83 | def connect(options = {}) 84 | connect!(options) 85 | rescue TimeoutError 86 | nil 87 | end 88 | 89 | # Public: Connects to the running server by name, blocking if need be. 90 | # Raises a TimeoutError if no server was found in the given time in 91 | # seconds. 92 | # 93 | # options - An optional Hash. For now, only used for specifying a timeout 94 | # (default: {}): 95 | # 96 | # :timeout - The Integer timeout to wait for a running server 97 | # (default: 5). 98 | # 99 | # Returns a new Client instance initialized with this Server. 100 | def connect!(options = {}) 101 | wait_until_running(options[:timeout] || 5) 102 | 103 | client = new_client 104 | client.source(VIMRUNNER_RC) 105 | client 106 | end 107 | 108 | # Public: Checks if there's a running Vim instance with the server's name. 109 | # 110 | # Returns a Boolean 111 | def running? 112 | serverlist.include?(name) 113 | end 114 | 115 | # Public: Kills the Vim instance in the background. 116 | # 117 | # Returns self. 118 | def kill 119 | @r.close 120 | @w.close 121 | 122 | begin 123 | Process.kill(9, @pid) 124 | rescue Errno::ESRCH 125 | end 126 | 127 | self 128 | end 129 | 130 | # Public: A convenience method that returns a new Client instance, 131 | # connected to this server. 132 | # 133 | # Returns a Client. 134 | def new_client 135 | Client.new(self) 136 | end 137 | 138 | # Public: Retrieves a list of names of currently running Vim servers. 139 | # 140 | # Returns an Array of String server names currently running. 141 | def serverlist 142 | execute([executable, "--serverlist"]).split("\n") 143 | end 144 | 145 | # Public: Evaluates an expression in the Vim server and returns the result. 146 | # A wrapper around --remote-expr. 147 | # 148 | # expression - a String with a Vim expression to evaluate. 149 | # 150 | # Returns the String output of the expression. 151 | def remote_expr(expression) 152 | execute([executable, "--servername", name, "--remote-expr", expression]) 153 | end 154 | 155 | # Public: Sends the given keys 156 | # A wrapper around --remote-send. 157 | # 158 | # keys - a String with a sequence of Vim-compatible keystrokes. 159 | # 160 | # Returns nothing. 161 | def remote_send(keys) 162 | execute([executable, "--servername", name, "--remote-send", keys]) 163 | end 164 | 165 | private 166 | 167 | def foreground_option 168 | @foreground ? '-f' : '' 169 | end 170 | 171 | def execute(command) 172 | out, err, status = Open3.capture3(*command) 173 | if not err.empty? 174 | raise ExecutionError.new("Command failed (#{command}), output: #{err}") 175 | end 176 | 177 | out.chomp.gsub(/\A\n/, '') 178 | end 179 | 180 | def spawn 181 | PTY.spawn(executable, *%W[ 182 | #{foreground_option} 183 | --servername #{name} 184 | -u #{vimrc} 185 | -U #{gvimrc} 186 | -i NONE 187 | --noplugin 188 | ]) 189 | end 190 | 191 | def wait_until_running(seconds) 192 | Timeout.timeout(seconds, TimeoutError) do 193 | sleep 0.1 while !running? 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/vimrunner/testing.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | 3 | module Vimrunner 4 | 5 | # Public: Provides some utility helpers to assist in using Vimrunner for 6 | # testing purposes. 7 | module Testing 8 | 9 | # Public: Writes the given string to the file identified by "filename". 10 | # 11 | # Uses #normalize_string_indent to ensure consistent indentation when given 12 | # a heredoc, and takes care to write it in the same way that Vim would. 13 | # 14 | # filename - a String, the name of the file to write 15 | # string - a String, the contents of the file 16 | # 17 | # Returns nothing. 18 | def write_file(filename, string) 19 | string = normalize_string_indent(string) 20 | File.open(filename, 'w') { |f| f.write(string + "\n") } 21 | end 22 | 23 | # Public: Normalizes a string's indentation whitespace, so that heredocs 24 | # can be used more easily for testing. 25 | # 26 | # Example 27 | # 28 | # foo = normalize_string_indent(<<-EOF) 29 | # def foo 30 | # bar 31 | # end 32 | # EOF 33 | # 34 | # In this case, the raw string would have a large chunk of indentation in 35 | # the beginning due to its location within the code. The helper removes all 36 | # whitespace in the beginning by taking the one of the first line. 37 | # 38 | # Note: #scan and #chop are being used instead of #split to avoid 39 | # discarding empty lines. 40 | # 41 | # string - a String, usually defined using a heredoc 42 | # 43 | # Returns a String with reduced indentation. 44 | def normalize_string_indent(string) 45 | if string.end_with?("\n") 46 | lines = string.scan(/.*\n/).map(&:chop) 47 | whitespace = lines.grep(/\S/).first.scan(/^\s*/).first 48 | else 49 | lines = [string] 50 | whitespace = string.scan(/^\s*/).first 51 | end 52 | 53 | lines.map do |line| 54 | line.gsub(/^#{whitespace}/, '') if line =~ /\S/ 55 | end.join("\n") 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/vimrunner/version.rb: -------------------------------------------------------------------------------- 1 | module Vimrunner 2 | VERSION = '0.3.6' 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | require "simplecov" 3 | 4 | SimpleCov.start 5 | 6 | RSpec.configure do |config| 7 | def write_file(filename, contents) 8 | dirname = File.dirname(filename) 9 | FileUtils.mkdir_p dirname if not File.directory?(dirname) 10 | 11 | File.open(filename, 'w') { |f| f.write(contents) } 12 | end 13 | 14 | # Execute each example in its own temporary directory that is automatically 15 | # destroyed after every run. 16 | config.around do |example| 17 | Dir.mktmpdir do |dir| 18 | Dir.chdir(dir) do 19 | example.call 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/vimrunner/client_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "vimrunner" 3 | 4 | module Vimrunner 5 | RSpec.describe Client do 6 | let!(:client) { Vimrunner.start } 7 | 8 | after :each do 9 | client.kill 10 | end 11 | 12 | it "is instantiated in the current directory" do 13 | cwd = FileUtils.getwd 14 | expect(client.command(:pwd)).to eq(cwd) 15 | end 16 | 17 | it "can write a file through Vim" do 18 | client.edit 'some_file' 19 | client.insert 'Contents of the file' 20 | client.write 21 | 22 | expect(File.exist?('some_file')).to be(true) 23 | expect(File.read('some_file').strip).to eq('Contents of the file') 24 | end 25 | 26 | it "properly escapes filenames" do 27 | client.edit 'some file' 28 | client.write 29 | 30 | expect(File.exist?('some file')).to be(true) 31 | end 32 | 33 | it "can execute commands with a bang" do 34 | client.edit 'some_file' 35 | client.insert 'Contents of the file' 36 | client.edit! 'some_other_file' 37 | client.insert 'Contents of the other file' 38 | client.command :write 39 | 40 | expect(File.exist?('some_file')).to be(false) 41 | expect(File.exist?('some_other_file')).to be(true) 42 | expect(File.read('some_other_file').strip).to eq('Contents of the other file') 43 | end 44 | 45 | it "can add a plugin for Vim to use" do 46 | write_file 'example/plugin/test.vim', 'command Okay echo "OK"' 47 | client.add_plugin('example', 'plugin/test.vim') 48 | 49 | expect(client.command('Okay')).to eq('OK') 50 | end 51 | 52 | it "can append a directory to Vim's runtimepath" do 53 | write_file 'example/test.vim', 'echo "OK"' 54 | client.append_runtimepath('example') 55 | 56 | expect(client.command('runtime test.vim')).to eq('OK') 57 | end 58 | 59 | it "can prepend a directory to Vim's runtimepath, giving it priority" do 60 | write_file 'example_first/test.vim', 'echo "first"' 61 | write_file 'example_second/test.vim', 'echo "second"' 62 | client.append_runtimepath('example_first') 63 | client.prepend_runtimepath('example_second') 64 | 65 | expect(client.command('runtime test.vim')).to eq('second') 66 | end 67 | 68 | it "can chain several operations" do 69 | client.edit('some_file').insert('Contents').write 70 | expect(File.exist?('some_file')).to be(true) 71 | expect(File.read('some_file').strip).to eq('Contents') 72 | end 73 | 74 | describe "#set" do 75 | it "activates a boolean setting" do 76 | client.set 'expandtab' 77 | expect(client.command('echo &expandtab')).to eq('1') 78 | 79 | client.set 'noexpandtab' 80 | expect(client.command('echo &expandtab')).to eq('0') 81 | end 82 | 83 | it "sets a setting to a given value" do 84 | client.set 'tabstop', 3 85 | expect(client.command('echo &tabstop')).to eq('3') 86 | end 87 | 88 | it "can be chained" do 89 | client.set('expandtab').set('tabstop', 3) 90 | expect(client.command('echo &expandtab')).to eq('1') 91 | expect(client.command('echo &tabstop')).to eq('3') 92 | end 93 | end 94 | 95 | describe "#search" do 96 | before :each do 97 | client.edit 'some_file' 98 | client.insert 'one two' 99 | end 100 | 101 | it "positions the cursor on the search term" do 102 | client.search 'two' 103 | client.normal 'dw' 104 | 105 | client.write 106 | 107 | expect(File.read('some_file').strip).to eq('one') 108 | end 109 | 110 | it "can be chained" do 111 | client.search('two').search('one') 112 | client.normal 'dw' 113 | 114 | client.write 115 | 116 | expect(File.read('some_file').strip).to eq('two') 117 | end 118 | end 119 | 120 | describe "#echo" do 121 | it "returns the result of a given expression" do 122 | expect(client.echo('"foo"')).to eq('foo') 123 | end 124 | 125 | it "returns the result of multiple expressions" do 126 | client.command('let b:foo = "bar"') 127 | expect(client.echo('"foo"', 'b:foo')).to eq('foo bar') 128 | end 129 | end 130 | 131 | describe "#feedkeys" do 132 | before :each do 133 | client.edit 'some_file' 134 | client.command 'map ihello' 135 | end 136 | 137 | it "sends keys as if they come from a mapping or user" do 138 | client.feedkeys('\') 139 | client.write 140 | expect(File.read('some_file').strip).to eq('hello') 141 | end 142 | 143 | it "handles quotes" do 144 | client.feedkeys('\\'"') 145 | client.write 146 | expect(File.read('some_file').strip).to eq('hello\'"') 147 | end 148 | end 149 | 150 | describe "#command" do 151 | it "returns the output of a Vim command" do 152 | expect(client.command(:version)).to include('+clientserver') 153 | expect(client.command('echo "foo"')).to eq('foo') 154 | end 155 | 156 | it "allows single quotes in the command" do 157 | expect(client.command("echo 'foo'")).to eq('foo') 158 | end 159 | 160 | it "raises an error for a non-existent Vim command" do 161 | expect { 162 | client.command(:nonexistent) 163 | }.to raise_error(InvalidCommandError) 164 | end 165 | 166 | it "maintains whitespace in the output" do 167 | expect(client.command('echo " foo "')).to eq ' foo ' 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /spec/vimrunner/command_spec.rb: -------------------------------------------------------------------------------- 1 | require "vimrunner/command" 2 | 3 | module Vimrunner 4 | RSpec.describe Command do 5 | it "leaves standard commands untouched" do 6 | expect(Command.new("set linenumber").to_s).to eq("set linenumber") 7 | end 8 | 9 | it "escapes single quotes" do 10 | expect(Command.new("echo 'foo'").to_s).to eq("echo ''foo''") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/vimrunner/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "vimrunner" 3 | require "vimrunner/errors" 4 | 5 | module Vimrunner 6 | RSpec.describe NoSuitableVimError do 7 | it "has a useful message" do 8 | expect { 9 | raise NoSuitableVimError 10 | }.to raise_error(/No suitable Vim executable could be found for this system./) 11 | end 12 | end 13 | 14 | describe TimeoutError do 15 | it "has a useful message" do 16 | expect { 17 | raise TimeoutError 18 | }.to raise_error(/Timed out while waiting for serverlist. Is an X11 server running?/) 19 | end 20 | end 21 | 22 | describe InvalidCommandError do 23 | it "has a useful message" do 24 | expect { 25 | Vimrunner.start do |vim| 26 | vim.command :nonexistent 27 | end 28 | }.to raise_error(/Not an editor command: nonexistent/) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/vimrunner/path_spec.rb: -------------------------------------------------------------------------------- 1 | require "vimrunner/path" 2 | 3 | module Vimrunner 4 | RSpec.describe Path do 5 | it "leaves standard paths untouched" do 6 | expect(Path.new("foo.txt").to_s).to eq("foo.txt") 7 | end 8 | 9 | it "escapes non-standard characters in paths" do 10 | expect(Path.new("foo bar!.txt").to_s).to eq('foo\ bar\!.txt') 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/vimrunner/platform_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "vimrunner/platform" 3 | 4 | module Vimrunner 5 | RSpec.describe Platform do 6 | describe "#vim" do 7 | it "raises an error if no suitable vim could be found" do 8 | allow(Platform).to receive(:suitable?).and_return(false) 9 | 10 | expect { Platform.vim }.to raise_error(NoSuitableVimError) 11 | end 12 | 13 | it "returns vim if it supports clientserver and xterm_clipboard" do 14 | allow(Platform).to receive(:features).and_return("+clientserver +xterm_clipboard") 15 | 16 | expect(Platform.vim).to eq("vim") 17 | end 18 | 19 | it "returns gvim on Linux if vim doesn't support xterm_clipboard" do 20 | allow(Platform).to receive(:mac?).and_return(false) 21 | allow(Platform).to receive(:features) { |vim| 22 | case vim 23 | when "vim" 24 | "+clientserver -xterm_clipboard" 25 | when "gvim" 26 | "+clientserver" 27 | end 28 | } 29 | 30 | expect(Platform.vim).to eq("gvim") 31 | end 32 | 33 | it "returns mvim on Mac OS X if vim doesn't support clientserver" do 34 | allow(Platform).to receive(:mac?).and_return(true) 35 | allow(Platform).to receive(:features) { |vim| 36 | case vim 37 | when "vim" 38 | "-clientserver -xterm_clipboard" 39 | when "mvim" 40 | "+clientserver -xterm_clipboard" 41 | end 42 | } 43 | 44 | expect(Platform.vim).to eq("mvim") 45 | end 46 | 47 | it "ignores versions of vim that do not exist on the system" do 48 | allow(Platform).to receive(:mac?).and_return(false) 49 | allow(IO).to receive(:popen) { |command| 50 | if command == ["vim", "--version"] 51 | raise Errno::ENOENT 52 | else 53 | "+clientserver" 54 | end 55 | } 56 | 57 | expect(Platform.vim).to eq("gvim") 58 | end 59 | end 60 | 61 | describe "#gvim" do 62 | it "raises an error if no suitable gvim could be found" do 63 | allow(Platform).to receive(:suitable?).and_return(false) 64 | expect { Platform.gvim }.to raise_error(NoSuitableVimError) 65 | end 66 | 67 | it "returns gvim on Linux" do 68 | allow(Platform).to receive(:mac?).and_return(false) 69 | allow(Platform).to receive(:features).and_return("+clientserver") 70 | 71 | expect(Platform.gvim).to eq("gvim") 72 | end 73 | 74 | it "returns mvim on Mac OS X" do 75 | allow(Platform).to receive(:mac?).and_return(true) 76 | allow(Platform).to receive(:features).and_return("+clientserver") 77 | 78 | expect(Platform.gvim).to eq("mvim") 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/vimrunner/server_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "vimrunner/server" 3 | require "vimrunner/platform" 4 | 5 | module Vimrunner 6 | RSpec.describe Server do 7 | let(:server) { Server.new } 8 | 9 | describe "#initialize" do 10 | it "defaults to using Platform.vim for the executable" do 11 | expect(server.executable).to eq(Platform.vim) 12 | end 13 | 14 | it "defaults to a random name" do 15 | expect(server.name).to start_with("VIMRUNNER") 16 | end 17 | 18 | it "ensures that its server name is uppercase" do 19 | server = Vimrunner::Server.new(:name => "foo") 20 | expect(server.name).to eq("FOO") 21 | end 22 | end 23 | 24 | describe "#start" do 25 | it "starts a vim server process" do 26 | begin 27 | server.start 28 | expect(server.serverlist).to include(server.name) 29 | ensure 30 | server.kill 31 | expect(server.serverlist).to_not include(server.name) 32 | end 33 | end 34 | 35 | it "can start more than one vim server process" do 36 | begin 37 | first = Server.new 38 | second = Server.new 39 | 40 | first.start 41 | second.start 42 | 43 | expect(first.serverlist).to include(first.name, second.name) 44 | ensure 45 | first.kill 46 | second.kill 47 | end 48 | end 49 | 50 | it "can start a vim server process with a block" do 51 | server.start do |client| 52 | expect(server.serverlist).to include(server.name) 53 | end 54 | 55 | expect(server.serverlist).to_not include(server.name) 56 | end 57 | end 58 | 59 | describe "connecting to an existing server" do 60 | before(:each) do 61 | server.start 62 | end 63 | 64 | let(:second_server) { Server.new(:name => server.name) } 65 | 66 | it "returns a client" do 67 | expect(second_server.connect).to be_a(Client) 68 | expect(second_server.connect!).to be_a(Client) 69 | end 70 | 71 | it "returns a client connected to the named server" do 72 | expect(second_server.connect.server).to eq(second_server) 73 | expect(second_server.connect!.server).to eq(second_server) 74 | end 75 | 76 | describe "#connect" do 77 | it "returns nil if no server is found in :timeout seconds" do 78 | server = Server.new(:name => 'NONEXISTENT') 79 | expect(server.connect(:timeout => 0.1)).to be_nil 80 | end 81 | end 82 | 83 | describe "#connect!" do 84 | it "raises an error if no server is found in :timeout seconds" do 85 | server = Server.new(:name => 'NONEXISTENT') 86 | expect { 87 | server.connect!(:timeout => 0.1) 88 | }.to raise_error(TimeoutError) 89 | end 90 | end 91 | end 92 | 93 | describe "#running?" do 94 | it "returns true if the server started successfully" do 95 | server.start 96 | expect(server).to be_running 97 | end 98 | 99 | it "returns true if the given name corresponds to a running Vim instance" do 100 | server.start 101 | other_server = Server.new(:name => server.name) 102 | 103 | expect(other_server).to be_running 104 | end 105 | end 106 | 107 | describe "#new_client" do 108 | it "returns a client" do 109 | expect(server.new_client).to be_a(Client) 110 | end 111 | 112 | it "is attached to the server" do 113 | expect(server.new_client.server).to eq(server) 114 | end 115 | end 116 | 117 | describe "#remote_expr" do 118 | it "uses the server's executable to send remote expressions" do 119 | expect(server).to receive(:execute). 120 | with([server.executable, "--servername", server.name, 121 | "--remote-expr", "version"]) 122 | 123 | server.remote_expr("version") 124 | end 125 | 126 | it "fails with a ExecutionError if the executable writes anything to stderr" do 127 | expect(server).to receive(:name).and_return('WRONG_NAME') 128 | expect { 129 | server.remote_expr("version") 130 | }.to raise_error(ExecutionError, /E247:/) 131 | end 132 | end 133 | 134 | describe "#remote_send" do 135 | it "uses the server's executable to send remote keys" do 136 | expect(server).to receive(:execute). 137 | with([server.executable, "--servername", server.name, 138 | "--remote-send", "ihello"]) 139 | 140 | server.remote_send("ihello") 141 | end 142 | 143 | it "fails with a ExecutionError if the executable writes anything to stderr" do 144 | expect(server).to receive(:name).and_return('WRONG_NAME') 145 | expect { 146 | server.remote_send("ihello") 147 | }.to raise_error(ExecutionError, /E247:/) 148 | end 149 | end 150 | 151 | describe "#serverlist" do 152 | it "uses the server's executable to list servers" do 153 | expect(server).to receive(:execute). 154 | with([server.executable, "--serverlist"]).and_return("VIM") 155 | 156 | server.serverlist 157 | end 158 | 159 | it "splits the servers into an array" do 160 | allow(server).to receive(:execute).and_return("VIM\nVIM2") 161 | 162 | expect(server.serverlist).to eq(["VIM", "VIM2"]) 163 | end 164 | end 165 | 166 | describe "pid" do 167 | it "returns the pid of the server" do 168 | server.start 169 | expect(server.pid).not_to be(nil) 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /spec/vimrunner/testing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'vimrunner' 3 | require 'vimrunner/testing' 4 | 5 | module Vimrunner 6 | RSpec.describe Testing do 7 | include Testing 8 | 9 | specify "#normalize_string_indent" do 10 | sample = normalize_string_indent(<<-EOF) 11 | def foo 12 | bar 13 | end 14 | EOF 15 | 16 | expect(sample).to eq("def foo\n bar\nend") 17 | end 18 | 19 | specify "#write_file" do 20 | Vimrunner.start do |vim| 21 | write_file 'written_by_ruby.txt', <<-EOF 22 | def one 23 | two 24 | end 25 | EOF 26 | 27 | vim.edit 'written_by_vim.txt' 28 | vim.insert 'def one twoend' 29 | vim.write 30 | 31 | expect(IO.read('written_by_ruby.txt')).to eq(IO.read('written_by_vim.txt')) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/vimrunner_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "vimrunner" 3 | 4 | RSpec.describe Vimrunner do 5 | let(:server) { double('server').as_null_object } 6 | 7 | describe "#start" do 8 | before do 9 | allow(Vimrunner::Platform).to receive(:vim).and_return("vim") 10 | end 11 | 12 | it "defaults to using the platform vim" do 13 | expect(Vimrunner::Server).to receive(:new).with(:executable => "vim"). 14 | and_return(server) 15 | 16 | Vimrunner.start 17 | end 18 | end 19 | 20 | describe "#start_gvim" do 21 | before do 22 | allow(Vimrunner::Platform).to receive(:gvim).and_return("gvim") 23 | end 24 | 25 | it "defaults to using the platform gvim" do 26 | expect(Vimrunner::Server).to receive(:new).with(:executable => "gvim"). 27 | and_return(server) 28 | 29 | Vimrunner.start_gvim 30 | end 31 | end 32 | 33 | describe "#connect" do 34 | let(:server) { Vimrunner::Server.new } 35 | 36 | before(:each) do 37 | server.start 38 | end 39 | 40 | it "connects to an existing server by name" do 41 | vim = Vimrunner.connect(server.name) 42 | expect(vim.server.name).to eq(server.name) 43 | end 44 | 45 | after(:each) do 46 | server.kill 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /vim/vimrc: -------------------------------------------------------------------------------- 1 | set nocompatible 2 | 3 | filetype plugin on 4 | filetype indent on 5 | syntax on 6 | 7 | set noswapfile nobackup 8 | 9 | " remove default ~/.vim directories to avoid loading plugins 10 | set runtimepath-=~/.vim 11 | set runtimepath-=~/.config/vim 12 | set runtimepath-=~/.vim/after 13 | set runtimepath-=~/.config/vim/after 14 | -------------------------------------------------------------------------------- /vim/vimrunner.vim: -------------------------------------------------------------------------------- 1 | let has_error_handling_bugfix = 2 | \ v:version > 703 || 3 | \ v:version == 703 && has('patch860') 4 | 5 | if has_error_handling_bugfix 6 | function! VimrunnerEvaluateCommandOutput(command) 7 | let output = '' 8 | 9 | try 10 | if exists('*execute') 11 | let output = execute(a:command, 'silent') 12 | else 13 | redir => output 14 | silent exe a:command 15 | redir END 16 | 17 | let output = s:StripSilencedErrors(output) 18 | endif 19 | catch 20 | let output = v:exception 21 | endtry 22 | 23 | return output 24 | endfunction 25 | else 26 | " Use some fake error handling to provide at least rudimentary errors for 27 | " missing commands. 28 | function! VimrunnerEvaluateCommandOutput(command) 29 | let base_command = split(a:command, '\s\+')[0] 30 | let base_command = substitute(base_command, '!$', '', '') 31 | let base_command = substitute(base_command, '^\d\+', '', '') 32 | 33 | if !exists(':'.base_command) 34 | let output = 'Vim:E492: Not an editor command: '.base_command 35 | else 36 | redir => output 37 | silent exe a:command 38 | redir END 39 | endif 40 | 41 | return output 42 | endfunction 43 | endif 44 | 45 | " Remove errors from the output that have been silenced by :silent!. These are 46 | " visible in the captured output since all messages are captured by :redir. 47 | function! s:StripSilencedErrors(output) 48 | let processed_output = [] 49 | 50 | for line in reverse(split(a:output, "\n")) 51 | if line =~ '^E\d\+:' 52 | break 53 | endif 54 | 55 | call add(processed_output, line) 56 | endfor 57 | 58 | return join(reverse(processed_output), "\n") 59 | endfunction 60 | 61 | " vim: set ft=vim 62 | -------------------------------------------------------------------------------- /vimrunner.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/vimrunner/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'vimrunner' 5 | s.version = Vimrunner::VERSION 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ['Andrew Radev', 'Paul Mucur'] 8 | s.email = ['andrey.radev@gmail.com'] 9 | s.homepage = 'http://github.com/AndrewRadev/vimrunner' 10 | s.summary = 'Lets you control a Vim instance through Ruby' 11 | s.description = <<-D 12 | Using Vim's client/server functionality, this library exposes a way to 13 | spawn a Vim instance and control it programatically. Apart from being a fun 14 | party trick, this can be used to integration test Vim script. 15 | D 16 | 17 | s.add_development_dependency 'rake' 18 | s.add_development_dependency 'rdoc' 19 | s.add_development_dependency 'simplecov' 20 | s.add_development_dependency 'rspec', '~> 3.7' 21 | 22 | s.required_rubygems_version = '>= 1.3.6' 23 | s.rubyforge_project = 'vimrunner' 24 | s.files = Dir['lib/**/*.rb', 'vim/*', 'bin/*', 'LICENSE', '*.md'] 25 | s.require_path = 'lib' 26 | s.executables = ['vimrunner'] 27 | end 28 | --------------------------------------------------------------------------------