Add Command-T files to integration tests
authorGreg Hurrell <greg@hurrell.net>
Sun, 12 Jun 2016 22:19:15 +0000 (15:19 -0700)
committerGreg Hurrell <greg@hurrell.net>
Sun, 12 Jun 2016 22:19:15 +0000 (15:19 -0700)
And yay, we've exposed a bug. Note that Command-T isn't
at all docvim-ready yet (no doc comments) but we still
shouldn't blow up trying to process it.

97 files changed:
bin/get
bin/put
tests/fixtures/integration/command-t/golden/ast.golden [new file with mode: 0644]
tests/fixtures/integration/command-t/golden/markdown.golden [new file with mode: 0644]
tests/fixtures/integration/command-t/golden/plaintext.golden [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.ctags [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.gitattributes [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.gitignore [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.gitmodules [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.mailmap [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.rspec [new file with mode: 0644]
tests/fixtures/integration/command-t/input/.vim_org.yml [new file with mode: 0644]
tests/fixtures/integration/command-t/input/Gemfile [new file with mode: 0644]
tests/fixtures/integration/command-t/input/Gemfile.lock [new file with mode: 0644]
tests/fixtures/integration/command-t/input/LICENSE [new file with mode: 0644]
tests/fixtures/integration/command-t/input/Rakefile [new file with mode: 0644]
tests/fixtures/integration/command-t/input/appstream/vim-command-t.metainfo.xml [new file with mode: 0644]
tests/fixtures/integration/command-t/input/autoload/commandt.vim [new file with mode: 0644]
tests/fixtures/integration/command-t/input/autoload/commandt/isengard.vim [new file with mode: 0644]
tests/fixtures/integration/command-t/input/autoload/commandt/mirkwood.vim [new file with mode: 0644]
tests/fixtures/integration/command-t/input/autoload/commandt/private.vim [new file with mode: 0644]
tests/fixtures/integration/command-t/input/bin/benchmarks/matcher.rb [new file with mode: 0755]
tests/fixtures/integration/command-t/input/bin/benchmarks/watchman.rb [new file with mode: 0755]
tests/fixtures/integration/command-t/input/fixtures/bar/abc [new file with mode: 0644]
tests/fixtures/integration/command-t/input/fixtures/bar/xyz [new file with mode: 0644]
tests/fixtures/integration/command-t/input/fixtures/baz [new file with mode: 0644]
tests/fixtures/integration/command-t/input/fixtures/bing [new file with mode: 0644]
tests/fixtures/integration/command-t/input/fixtures/foo/alpha/t1 [new file with mode: 0644]
tests/fixtures/integration/command-t/input/fixtures/foo/alpha/t2 [new file with mode: 0644]
tests/fixtures/integration/command-t/input/fixtures/foo/beta [new file with mode: 0644]
tests/fixtures/integration/command-t/input/plugin/command-t.vim [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/bin/commandtd [new file with mode: 0755]
tests/fixtures/integration/command-t/input/ruby/command-t/command-t.gemspec [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/.gitignore [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/depend [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ext.c [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ext.h [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/extconf.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/heap.c [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/heap.h [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/match.c [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/match.h [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/matcher.c [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/matcher.h [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ruby_compat.h [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/watchman.c [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/watchman.h [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/controller.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/buffer_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/command_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/file_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/help_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/history_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/jump_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/line_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/mru_buffer_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/tag_finder.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/match_window.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/metadata/fallback.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/mru.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/path_utilities.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/progress_reporter.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/prompt.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/buffer_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/command_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/find_file_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/git_file_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/ruby_file_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/watchman_file_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/help_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/history_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/jump_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/line_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/mru_buffer_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/tag_scanner.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scm_utilities.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/settings.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/stub.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/util.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim/screen.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim/window.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/controller_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/finder/buffer_finder_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/finder/file_finder_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/matcher_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/scanner/buffer_scanner_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner/ruby_file_scanner_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner/watchman_file_scanner_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/vim_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/command-t/watchman/utils_spec.rb [new file with mode: 0644]
tests/fixtures/integration/command-t/input/spec/spec_helper.rb [new file with mode: 0644]

diff --git a/bin/get b/bin/get
index 09ea6309fd7f570139f1a300ac03d2f5875478e4..5aa587c52dae6719f91051ce5b444b8ae38995b9 100755 (executable)
--- a/bin/get
+++ b/bin/get
@@ -11,6 +11,7 @@ usage() {
   echo
   echo "Usage:"
   echo
+  echo "  bin/get command-t   #"
   echo "  bin/get ferret      #"
   echo "  bin/put pinnacle    # freshen a specific downstream"
   echo "  bin/put scalpel     #"
@@ -37,12 +38,13 @@ else
     shift
     case $PROJECT in
       all)
+        update command-t
         update ferret
         update pinnacle
         update scalpel
         update vim-docvim
         ;;
-      ferret|pinnacle|scalpel|vim-docvim)
+      command-t|ferret|pinnacle|scalpel|vim-docvim)
         update "$PROJECT"
         ;;
       *)
diff --git a/bin/put b/bin/put
index ba7a81fac60b693b8842f1568a9f89b38ceb5720..d89c9a6b716f154d78d1e2d8218e55f26fa2dbcc 100755 (executable)
--- a/bin/put
+++ b/bin/put
@@ -17,6 +17,7 @@ usage() {
   echo
   echo "Usage:"
   echo
+  echo "  bin/put command-t   #"
   echo "  bin/put ferret      #"
   echo "  bin/put pinnacle    # freshen a specific downstream"
   echo "  bin/put scalpel     #"
@@ -45,12 +46,13 @@ else
     shift
     case $PROJECT in
       all)
+        update command-t
         update ferret
         update pinnacle
         update scalpel
         update vim-docvim
         ;;
-      ferret|pinnacle|scalpel|vim-docvim)
+      command-t|ferret|pinnacle|scalpel|vim-docvim)
         update "$PROJECT"
         ;;
       *)
diff --git a/tests/fixtures/integration/command-t/golden/ast.golden b/tests/fixtures/integration/command-t/golden/ast.golden
new file mode 100644 (file)
index 0000000..2546828
--- /dev/null
@@ -0,0 +1,3 @@
+"(eval)" (line 134, column 38):
+unexpected "_"
+expecting letter or digit, "\n", "," or ")"
diff --git a/tests/fixtures/integration/command-t/golden/markdown.golden b/tests/fixtures/integration/command-t/golden/markdown.golden
new file mode 100644 (file)
index 0000000..2546828
--- /dev/null
@@ -0,0 +1,3 @@
+"(eval)" (line 134, column 38):
+unexpected "_"
+expecting letter or digit, "\n", "," or ")"
diff --git a/tests/fixtures/integration/command-t/golden/plaintext.golden b/tests/fixtures/integration/command-t/golden/plaintext.golden
new file mode 100644 (file)
index 0000000..2546828
--- /dev/null
@@ -0,0 +1,3 @@
+"(eval)" (line 134, column 38):
+unexpected "_"
+expecting letter or digit, "\n", "," or ")"
diff --git a/tests/fixtures/integration/command-t/input/.ctags b/tests/fixtures/integration/command-t/input/.ctags
new file mode 100644 (file)
index 0000000..0ead77a
--- /dev/null
@@ -0,0 +1,3 @@
+--exclude=.git
+--exclude=Makefile
+--exclude=vendor
diff --git a/tests/fixtures/integration/command-t/input/.gitattributes b/tests/fixtures/integration/command-t/input/.gitattributes
new file mode 100644 (file)
index 0000000..4bf3ca8
--- /dev/null
@@ -0,0 +1 @@
+data/benchmark.yml export-ignore
diff --git a/tests/fixtures/integration/command-t/input/.gitignore b/tests/fixtures/integration/command-t/input/.gitignore
new file mode 100644 (file)
index 0000000..f89f0b3
--- /dev/null
@@ -0,0 +1,5 @@
+.release-notes.txt
+.ruby-version
+/.bundle
+/data/log.yml
+vendor/bundle
diff --git a/tests/fixtures/integration/command-t/input/.gitmodules b/tests/fixtures/integration/command-t/input/.gitmodules
new file mode 100644 (file)
index 0000000..ddb03f4
--- /dev/null
@@ -0,0 +1,6 @@
+[submodule "vendor/vimscriptuploader"]
+       path = vendor/vimscriptuploader
+       url = https://github.com/tomtom/vimscriptuploader.rb.git
+[submodule "vendor/vroom"]
+       path = vendor/vroom
+       url = https://github.com/google/vroom.git
diff --git a/tests/fixtures/integration/command-t/input/.mailmap b/tests/fixtures/integration/command-t/input/.mailmap
new file mode 100644 (file)
index 0000000..770b44b
--- /dev/null
@@ -0,0 +1,11 @@
+Greg Hurrell <greg@hurrell.net> <win@wincent.com>
+Jerome Castaneda <djjcast@gmail.com>
+Kevin Webster <webster.kevin@gmail.com>
+Kien Nguyen Duc <contact@ndkien.com>
+Nicholas T. <Nicholas.TD07@gmail.com>
+Nicolas Alpi <nicolas.alpi@gmail.com>
+Nikolai Aleksandrovich Pavlov <kp-pav@yandex.ru>
+Noon Silk <noonsilk@gmail.com> <superhappyfun@gmail.com>
+Sherzod Gapirov <sherzod.gapirov@mail.ru>
+Sung Pae <sung@metablu.com> <self@sungpae.com>
+Sung Pae <sung@metablu.com> guns <sung@metablu.com>
diff --git a/tests/fixtures/integration/command-t/input/.rspec b/tests/fixtures/integration/command-t/input/.rspec
new file mode 100644 (file)
index 0000000..53607ea
--- /dev/null
@@ -0,0 +1 @@
+--colour
diff --git a/tests/fixtures/integration/command-t/input/.vim_org.yml b/tests/fixtures/integration/command-t/input/.vim_org.yml
new file mode 100644 (file)
index 0000000..20670d5
--- /dev/null
@@ -0,0 +1,2 @@
+--- {}
+
diff --git a/tests/fixtures/integration/command-t/input/Gemfile b/tests/fixtures/integration/command-t/input/Gemfile
new file mode 100644 (file)
index 0000000..3afc8e2
--- /dev/null
@@ -0,0 +1,7 @@
+source 'https://rubygems.org'
+
+gem 'nokogiri'
+gem 'mechanize'
+gem 'rake'
+gem 'rr'
+gem 'rspec'
diff --git a/tests/fixtures/integration/command-t/input/Gemfile.lock b/tests/fixtures/integration/command-t/input/Gemfile.lock
new file mode 100644 (file)
index 0000000..a695d0b
--- /dev/null
@@ -0,0 +1,48 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    diff-lcs (1.2.5)
+    domain_name (0.5.15)
+      unf (>= 0.0.5, < 1.0.0)
+    http-cookie (1.0.2)
+      domain_name (~> 0.5)
+    mechanize (2.7.3)
+      domain_name (~> 0.5, >= 0.5.1)
+      http-cookie (~> 1.0)
+      mime-types (~> 2.0)
+      net-http-digest_auth (~> 1.1, >= 1.1.1)
+      net-http-persistent (~> 2.5, >= 2.5.2)
+      nokogiri (~> 1.4)
+      ntlm-http (~> 0.1, >= 0.1.1)
+      webrobots (>= 0.0.9, < 0.2)
+    mime-types (2.1)
+    mini_portile (0.5.2)
+    net-http-digest_auth (1.4)
+    net-http-persistent (2.9.4)
+    nokogiri (1.6.1)
+      mini_portile (~> 0.5.0)
+    ntlm-http (0.1.1)
+    rake (10.1.1)
+    rr (1.1.2)
+    rspec (2.14.1)
+      rspec-core (~> 2.14.0)
+      rspec-expectations (~> 2.14.0)
+      rspec-mocks (~> 2.14.0)
+    rspec-core (2.14.7)
+    rspec-expectations (2.14.5)
+      diff-lcs (>= 1.1.3, < 2.0)
+    rspec-mocks (2.14.5)
+    unf (0.1.3)
+      unf_ext
+    unf_ext (0.0.6)
+    webrobots (0.1.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  mechanize
+  nokogiri
+  rake
+  rr
+  rspec
diff --git a/tests/fixtures/integration/command-t/input/LICENSE b/tests/fixtures/integration/command-t/input/LICENSE
new file mode 100644 (file)
index 0000000..34af534
--- /dev/null
@@ -0,0 +1,22 @@
+Copyright 2010-present Greg Hurrell. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/tests/fixtures/integration/command-t/input/Rakefile b/tests/fixtures/integration/command-t/input/Rakefile
new file mode 100644 (file)
index 0000000..a38bfcb
--- /dev/null
@@ -0,0 +1,184 @@
+require 'yaml'
+
+def bail_on_failure
+  exitstatus = $?.exitstatus
+  if exitstatus != 0
+    err "last command failed with exit status #{exitstatus}"
+    exit 1
+  end
+end
+
+def version
+  `git describe`.chomp
+end
+
+def rubygems_version
+  # RubyGems will barf if we try to pass an intermediate version number
+  # like "1.1b2-10-g61a374a", so no choice but to abbreviate it
+  `git describe --abbrev=0`.chomp
+end
+
+def yellow
+  "\033[33m"
+end
+
+def red
+  "\033[31m"
+end
+
+def clear
+  "\033[0m"
+end
+
+def warn(str)
+  puts "#{yellow}warning: #{str}#{clear}"
+end
+
+def err(str)
+  puts "#{red}error: #{str}#{clear}"
+end
+
+task :default => :help
+
+desc 'Print help on preparing a release'
+task :help do
+  puts <<-END
+
+The general release sequence is:
+
+  rake prerelease
+  rake gem
+  rake push
+  rake upload:all
+
+For a full list of available tasks:
+
+  rake -T
+
+  END
+end
+
+desc 'Run specs'
+task :spec do
+  system 'bundle exec rspec spec'
+  bail_on_failure
+end
+
+desc 'Create archive'
+task :archive => :check_tag do
+  system "git archive -o command-t-#{version}.zip HEAD -- ."
+  bail_on_failure
+end
+
+desc 'Clean compiled products'
+task :clean do
+  Dir.chdir 'ruby/command-t/ext/command-t' do
+    system 'make clean' if File.exists?('Makefile')
+    system 'rm -f Makefile'
+  end
+end
+
+desc 'Compile extension'
+task :make do
+  Dir.chdir 'ruby/command-t/ext/command-t' do
+    ruby 'extconf.rb'
+    system 'make clean'
+    bail_on_failure
+    system 'make'
+    bail_on_failure
+  end
+end
+
+desc 'Check that the current HEAD is tagged'
+task :check_tag do
+  unless system 'git describe --exact-match HEAD 2> /dev/null'
+    warn 'current HEAD is not tagged'
+  end
+end
+
+desc 'Verify that required dependencies are installed'
+task :check_deps do
+  begin
+    require 'rubygems'
+    require 'mechanize'
+  rescue LoadError
+    warn 'mechanize not installed (`gem install mechanize` in order to upload)'
+  end
+end
+
+desc 'Run checks prior to release'
+task :prerelease => [:make, :spec, :archive, :check_tag, :check_deps]
+
+desc 'Prepare release notes from HISTORY'
+task :notes do
+  File.open('.release-notes.txt', 'w') do |out|
+    lines = File.readlines('doc/command-t.txt').each(&:chomp!)
+    while line = lines.shift do
+      next unless line =~ /^HISTORY +\*command-t-history\*$/
+      break unless lines.shift == '' &&
+                  (line = lines.shift) && line =~ /^\d\.\d/ &&
+                  lines.shift == ''
+      while (line = lines.shift) && line != ''
+        out.puts line
+      end
+      break
+    end
+    out.puts ''
+    out.puts '# Please edit the release notes to taste.'
+    out.puts '# Blank lines and lines beginning with a hash will be removed.'
+    out.puts '# To abort, exit your editor with a non-zero exit status (:cquit in Vim).'
+  end
+
+  unless system "$EDITOR .release-notes.txt"
+    err "editor exited with non-zero exit status; aborting"
+    exit 1
+  end
+
+  filtered = File.readlines('.release-notes.txt').reject do |line|
+    line =~ /^(#.*|\s*)$/ # filter comment lines and blank lines
+  end.join
+
+  File.open('.release-notes.txt', 'w') do |out|
+    out.print filtered
+  end
+end
+
+namespace :upload do
+  desc 'Upload current archive to Amazon S3'
+  task :s3 => :archive do
+    sh 'aws --curl-options=--insecure put ' +
+      "s3.wincent.com/command-t/releases/command-t-#{version}.zip " +
+      "command-t-#{version}.zip"
+    sh 'aws --curl-options=--insecure put ' +
+      "s3.wincent.com/command-t/releases/command-t-#{version}.zip?acl " +
+      '--public'
+  end
+
+  desc 'Upload current archive to www.vim.org'
+  task :vim => [:archive, :notes] do
+    sh "vendor/vimscriptuploader/vimscriptuploader.rb \
+            --id 3025 \
+            --file command-t-#{version}.zip \
+            --message-file .release-notes.txt \
+            --version #{version} \
+            --config ~/.vim_org.yml \
+            .vim_org.yml"
+  end
+
+  desc 'Upload current archive everywhere'
+  task :all => [:s3, :vim]
+end
+
+desc 'Create the ruby gem package'
+task :gem => :check_tag do
+  Dir.chdir 'ruby/command-t' do
+    sh "gem build command-t.gemspec"
+  end
+end
+
+desc 'Push gem to Gemcutter ("gem push")'
+task :push => :gem do
+  Dir.chdir 'ruby/command-t' do
+    sh "gem push command-t-#{rubygems_version}.gem"
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/appstream/vim-command-t.metainfo.xml b/tests/fixtures/integration/command-t/input/appstream/vim-command-t.metainfo.xml
new file mode 100644 (file)
index 0000000..5380787
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2014 Vít Ondruch <v.ondruch@gmail.com> -->
+<component type="addon">
+<id>vim-command-t</id>
+<extends>gvim.desktop</extends>
+<name>command-t</name>
+<summary>Provides an extremely fast, intuitive mechanism for opening files with a minimal number of keystrokes</summary>
+<url type="homepage">https://github.com/wincent/command-t</url>
+<metadata_license>CC0-1.0</metadata_license>
+<project_license>BSD</project_license>
+<updatecontact>v.ondruch@gmail.com</updatecontact>
+</component>
diff --git a/tests/fixtures/integration/command-t/input/autoload/commandt.vim b/tests/fixtures/integration/command-t/input/autoload/commandt.vim
new file mode 100644 (file)
index 0000000..b08cf1b
--- /dev/null
@@ -0,0 +1,188 @@
+" Copyright 2010-present Greg Hurrell. All rights reserved.
+" Licensed under the terms of the BSD 2-clause license.
+
+if exists('g:command_t_autoloaded') || &cp
+  finish
+endif
+let g:command_t_autoloaded = 1
+
+"
+" Functions
+"
+
+function! s:RubyWarning() abort
+  echohl WarningMsg
+  echo 'command-t.vim requires Vim to be compiled with Ruby support'
+  echo 'For more information type:  :help command-t'
+  echohl none
+endfunction
+
+function! commandt#BufferFinder() abort
+  if has('ruby')
+    ruby $command_t.show_buffer_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#CommandFinder() abort
+  if has('ruby')
+    ruby $command_t.show_command_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#FileFinder(arg) abort
+  if has('ruby')
+    ruby $command_t.show_file_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#JumpFinder() abort
+  if has('ruby')
+    ruby $command_t.show_jump_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#MRUFinder() abort
+  if has('ruby')
+    ruby $command_t.show_mru_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#HelpFinder() abort
+  if has('ruby')
+    ruby $command_t.show_help_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#HistoryFinder() abort
+  if has('ruby')
+    ruby $command_t.show_history_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#LineFinder() abort
+  if has('ruby')
+    let g:CommandTCurrentBuffer=bufnr('%')
+    ruby $command_t.show_line_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#SearchFinder() abort
+  if has('ruby')
+    ruby $command_t.show_search_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#TagFinder() abort
+  if has('ruby')
+    ruby $command_t.show_tag_finder
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#Flush() abort
+  if has('ruby')
+    ruby $command_t.flush
+  else
+    call s:RubyWarning()
+  endif
+endfunction
+
+function! commandt#Load() abort
+  if !has('ruby')
+    call s:RubyWarning()
+  endif
+endfunction
+
+" For possible use in status lines.
+function! commandt#ActiveFinder() abort
+  if has('ruby')
+    ruby ::VIM::command "return '#{$command_t.active_finder}'"
+  else
+    return ''
+  endif
+endfunction
+
+" For possible use in status lines.
+function! commandt#Path() abort
+  if has('ruby')
+    ruby ::VIM::command "return '#{($command_t.path || '').gsub(/'/, "''")}'"
+  else
+    return ''
+  endif
+endfunction
+
+" For possible use in status lines.
+function! commandt#CheckBuffer(buffer_number) abort
+  if has('ruby')
+    execute 'ruby $command_t.return_is_own_buffer' a:buffer_number
+  else
+    return 0
+  endif
+endfunction
+
+if !has('ruby')
+  finish
+endif
+
+" note that we only start tracking buffers from first (autoloaded) use of Command-T
+augroup CommandTMRUBuffer
+  autocmd!
+  autocmd BufEnter * ruby CommandT::MRU.touch
+  autocmd BufDelete * ruby CommandT::MRU.delete
+augroup END
+
+ruby << EOF
+  # require Ruby files
+  begin
+    require 'command-t'
+
+    # Make sure we're running with the same version of Ruby that Command-T was
+    # compiled with.
+    patchlevel = defined?(RUBY_PATCHLEVEL) ? RUBY_PATCHLEVEL : nil
+    if CommandT::Metadata::UNKNOWN == true || (
+      CommandT::Metadata::EXPECTED_RUBY_VERSION == RUBY_VERSION &&
+      CommandT::Metadata::EXPECTED_RUBY_PATCHLEVEL == patchlevel
+    )
+      require 'command-t/ext' # eager load, to catch compilation problems early
+      $command_t = CommandT::Controller.new
+    else
+      $command_t = CommandT::Stub.new
+    end
+  rescue LoadError
+    load_path_modified = false
+    ::VIM::evaluate('&runtimepath').to_s.split(',').each do |path|
+      ext = "#{path}/ruby/command-t/ext"
+      if !$LOAD_PATH.include?(ext) && File.exist?(ext)
+        $LOAD_PATH << ext
+        load_path_modified = true
+      end
+      lib = "#{path}/ruby/command-t/lib"
+      if !$LOAD_PATH.include?(lib) && File.exist?(lib)
+        $LOAD_PATH << lib
+        load_path_modified = true
+      end
+    end
+    retry if load_path_modified
+
+    $command_t = CommandT::Stub.new
+  end
+EOF
diff --git a/tests/fixtures/integration/command-t/input/autoload/commandt/isengard.vim b/tests/fixtures/integration/command-t/input/autoload/commandt/isengard.vim
new file mode 100644 (file)
index 0000000..581c343
--- /dev/null
@@ -0,0 +1,25 @@
+" Copyright 2010-present Greg Hurrell. All rights reserved.
+" Licensed under the terms of the BSD 2-clause license.
+
+let s:script_directory=expand('<sfile>:p:h')
+
+" Set up the new async implementation of the Command-T engine -- successor to
+" "mirkwood" -- codenamed "isengard".
+function! commandt#isengard#init() abort
+  let l:daemon_path=resolve(s:script_directory . '/../../ruby/command-t/bin/commandtd')
+
+  let l:client_log_file=get(g:, 'CommandTClientLog', '')
+  let l:server_log_file=get(g:, 'CommandTServerLog', '')
+  if !empty(l:client_log_file)
+    call ch_logfile(l:client_log_file, 'w')
+  endif
+  if !empty(l:server_log_file)
+    let s:job=job_start([l:daemon_path, '--logfile=' . l:server_log_file, '--vim-pid=' . getpid()])
+  else
+    let s:job=job_start([l:daemon_path, '--vim-pid=' . getpid()])
+  endif
+  let s:channel=job_getchannel(s:job)
+
+  call ch_evalraw(s:channel, json_encode({'cd': getcwd()}) . "\n")
+  let g:CommandTResult=ch_evalraw(s:channel, json_encode({'match': 'commandt'}) . "\n")
+endfunction
diff --git a/tests/fixtures/integration/command-t/input/autoload/commandt/mirkwood.vim b/tests/fixtures/integration/command-t/input/autoload/commandt/mirkwood.vim
new file mode 100644 (file)
index 0000000..752de03
--- /dev/null
@@ -0,0 +1,44 @@
+" Copyright 2010-present Greg Hurrell. All rights reserved.
+" Licensed under the terms of the BSD 2-clause license.
+
+" Set up the original implementation Command-T engine, codenamed "mirkwood".
+function! commandt#mirkwood#init() abort
+  command! CommandTBuffer call commandt#BufferFinder()
+  command! CommandTCommand call commandt#CommandFinder()
+  command! CommandTHelp call commandt#HelpFinder()
+  command! CommandTHistory call commandt#HistoryFinder()
+  command! CommandTJump call commandt#JumpFinder()
+  command! CommandTLine call commandt#LineFinder()
+  command! CommandTMRU call commandt#MRUFinder()
+  command! CommandTSearch call commandt#SearchFinder()
+  command! CommandTTag call commandt#TagFinder()
+  command! -nargs=? -complete=dir CommandT call commandt#FileFinder(<q-args>)
+  command! CommandTFlush call commandt#Flush()
+  command! CommandTLoad call commandt#Load()
+
+  if !hasmapto('<Plug>(CommandT)') && maparg('<Leader>t', 'n') ==# ''
+    nmap <unique> <Leader>t <Plug>(CommandT)
+  endif
+  nnoremap <silent> <Plug>(CommandT) :CommandT<CR>
+
+  if !hasmapto('<Plug>(CommandTBuffer)') && maparg('<Leader>b', 'n') ==# ''
+    nmap <unique> <Leader>b <Plug>(CommandTBuffer)
+  endif
+  nnoremap <silent> <Plug>(CommandTBuffer) :CommandTBuffer<CR>
+
+  nnoremap <silent> <Plug>(CommandTHelp) :CommandTHelp<CR>
+  nnoremap <silent> <Plug>(CommandTHistory) :CommandTHistory<CR>
+
+  if has('jumplist')
+    if !hasmapto('<Plug>(CommandTJump)') && maparg('<Leader>j', 'n') ==# ''
+      nmap <unique> <Leader>j <Plug>(CommandTJump)
+    endif
+    nnoremap <silent> <Plug>(CommandTJump) :CommandTJump<CR>
+  endif
+
+  nnoremap <silent> <Plug>(CommandTCommand) :CommandTCommand<CR>
+  nnoremap <silent> <Plug>(CommandTLine) :CommandTLine<CR>
+  nnoremap <silent> <Plug>(CommandTMRU) :CommandTMRU<CR>
+  nnoremap <silent> <Plug>(CommandTSearch) :CommandTSearch<CR>
+  nnoremap <silent> <Plug>(CommandTTag) :CommandTTag<CR>
+endfunction
diff --git a/tests/fixtures/integration/command-t/input/autoload/commandt/private.vim b/tests/fixtures/integration/command-t/input/autoload/commandt/private.vim
new file mode 100644 (file)
index 0000000..1a2c472
--- /dev/null
@@ -0,0 +1,90 @@
+" Copyright 2010-present Greg Hurrell. All rights reserved.
+" Licensed under the terms of the BSD 2-clause license.
+
+function! commandt#private#ListMatches() abort
+  ruby $command_t.list_matches
+endfunction
+
+function! commandt#private#HandleKey(arg) abort
+  ruby $command_t.handle_key
+endfunction
+
+function! commandt#private#Backspace() abort
+  ruby $command_t.backspace
+endfunction
+
+function! commandt#private#Delete() abort
+  ruby $command_t.delete
+endfunction
+
+function! commandt#private#AcceptSelection() abort
+  ruby $command_t.accept_selection
+endfunction
+
+function! commandt#private#AcceptSelectionTab() abort
+  ruby $command_t.accept_selection :command => $command_t.tab_command
+endfunction
+
+function! commandt#private#AcceptSelectionSplit() abort
+  ruby $command_t.accept_selection :command => $command_t.split_command
+endfunction
+
+function! commandt#private#AcceptSelectionVSplit() abort
+  ruby $command_t.accept_selection :command => $command_t.vsplit_command
+endfunction
+
+function! commandt#private#Quickfix() abort
+  ruby $command_t.quickfix
+endfunction
+
+function! commandt#private#Refresh() abort
+  ruby $command_t.refresh
+endfunction
+
+function! commandt#private#ToggleFocus() abort
+  ruby $command_t.toggle_focus
+endfunction
+
+function! commandt#private#Cancel() abort
+  ruby $command_t.cancel
+endfunction
+
+function! commandt#private#SelectNext() abort
+  ruby $command_t.select_next
+endfunction
+
+function! commandt#private#SelectPrev() abort
+  ruby $command_t.select_prev
+endfunction
+
+function! commandt#private#Clear() abort
+  ruby $command_t.clear
+endfunction
+
+function! commandt#private#ClearPrevWord() abort
+  ruby $command_t.clear_prev_word
+endfunction
+
+function! commandt#private#CursorLeft() abort
+  ruby $command_t.cursor_left
+endfunction
+
+function! commandt#private#CursorRight() abort
+  ruby $command_t.cursor_right
+endfunction
+
+function! commandt#private#CursorEnd() abort
+  ruby $command_t.cursor_end
+endfunction
+
+function! commandt#private#CursorStart() abort
+  ruby $command_t.cursor_start
+endfunction
+
+function! commandt#private#RunAutocmd(cmd) abort
+  if v:version > 703 || v:version == 703 && has('patch438')
+    execute 'silent doautocmd <nomodeline> User ' . a:cmd
+  else
+    execute 'silent doautocmd User ' . a:cmd
+  endif
+endfunction
diff --git a/tests/fixtures/integration/command-t/input/bin/benchmarks/matcher.rb b/tests/fixtures/integration/command-t/input/bin/benchmarks/matcher.rb
new file mode 100755 (executable)
index 0000000..871d274
--- /dev/null
@@ -0,0 +1,268 @@
+#!/usr/bin/env ruby
+#
+# Copyright 2013-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+lib  = File.expand_path('../../ruby', File.dirname(__FILE__))
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+
+require 'command-t/ext'
+require 'command-t/util'
+require 'benchmark'
+require 'ostruct'
+require 'yaml'
+
+data = YAML.load_file(
+  File.expand_path('../../data/benchmark.yml', File.dirname(__FILE__))
+)
+log = File.expand_path('../../data/log.yml', File.dirname(__FILE__))
+log_data = File.exist?(log) ? YAML.load_file(log) : []
+
+threads = CommandT::Util.processor_count
+
+puts "Starting benchmark run (PID: #{Process.pid})"
+now = Time.now.to_s
+
+TIMES = ENV.fetch('TIMES', 20).to_i
+results = TIMES.times.map do
+  Benchmark.bmbm do |b|
+    data['tests'].each do |test|
+      scanner = OpenStruct.new(:paths => test['paths'])
+      matcher = CommandT::Matcher.new(scanner)
+      b.report(test['name']) do
+        test['times'].times do
+          test['queries'].each do |query|
+            query.split(//).reduce('') do |acc, char|
+              query = acc + char
+              matcher.sorted_matches_for(
+                query,
+                :threads => threads,
+                :recurse => ENV.fetch('RECURSE', '1') == '1'
+              )
+              query
+            end
+          end
+        end
+      end
+    end
+  end
+end
+
+DIFFERENCE = 0
+ABSOLUTE = 1
+SIGN = 2
+
+# Test for significance via Wilcoxon Signed Rank test.
+#
+# @see http://vassarstats.net/textbook/ch12a.html
+def significance(last, current)
+  return 0.0 if last.length != current.length
+
+  table = last.zip(current).map do |l, c|
+    [
+      l - c, # difference
+      (l - c).abs, # absolute difference
+      (l - c).zero? ? nil : (l - c) / (l - c).abs, # signedness (-1 or +1)
+    ]
+  end
+  table = table.select { |diff, abs, sig| !diff.zero? }
+  table = table.sort do |(a_diff, a_abs, a_sig), (b_diff, b_abs, b_sig)|
+    a_abs <=> b_abs
+  end
+
+  rank = 1
+  table = table.map do |row|
+    count = 0
+    rank = table.map.with_index do |(diff, abs, sig), i|
+      if abs == row[ABSOLUTE]
+        count += 1
+        i + 1
+      else
+        nil
+      end
+    end.compact.reduce(0) { |acc, val| acc + val }.to_f / count
+    row + [row[SIGN] * rank]
+  end
+
+  n = table.length
+  w = table.reduce(0) { |acc, (diff, abs, sig, signed_rank)| acc + signed_rank }
+
+  if n < 10
+    p_value = 0
+    thresholds = [
+      [],
+      [],
+      [],
+      [],
+      [],
+      [[15, 0.05]],
+      [[17, 0.05], [21, 0.025]],
+      [[22, 0.05], [25, 0.025], [28, 0.01]],
+      [[26, 0.05], [30, 0.025], [34, 0.01], [36, 0.005]],
+      [[29, 0.05], [35, 0.025], [39, 0.01], [43, 0.005]],
+    ][n]
+    while limit = thresholds.pop do
+      if w.abs >= limit[0]
+        p_value = limit[1]
+        break
+      end
+    end
+  else
+    sd = Math.sqrt(n * (n + 1) *  (2 * n + 1) / 6)
+    z = ((w - 0.5) / sd).abs
+    if z >= 3.291
+      p_value = 0.0005
+    elsif z >= 2.576
+      p_value = 0.005
+    elsif z >= 2.326
+      p_value = 0.01
+    elsif z >= 1.960
+      p_value = 0.025
+    elsif z >= 1.645
+      p_value = 0.05
+    else
+      p_value = 0
+    end
+  end
+
+  p_value
+end
+
+results = results.reduce({}) do |acc, run|
+  run.each do |result|
+    acc[result.label] ||= {}
+    acc[result.label]['real'] ||= []
+    acc[result.label]['real'] << result.real
+    acc[result.label]['total'] ||= []
+    acc[result.label]['total'] << result.total
+  end
+  acc
+end
+
+previous = YAML.load_file(log).last['results'] rescue nil
+
+results.keys.each do |label|
+  test = results[label]
+
+  test['real (best)'] = test['real'].min
+  test['total (best)'] = test['total'].min
+
+  test['real (avg)'] = test['real'].reduce(:+) / test['real'].length
+  test['real (+/-)'] = previous && previous[label] &&
+    (test['real (avg)'] - previous[label]['real (avg)']) / test['real (avg)'] * 100
+  test['real (significance)'] = significance(previous[label]['real'], test['real']) if previous && previous[label]
+  test['total (avg)'] = test['total'].reduce(:+) / test['total'].length
+  test['total (+/-)'] = previous && previous[label] &&
+    (test['total (avg)'] - previous[label]['total (avg)']) / test['total (avg)'] * 100
+  test['total (significance)'] = significance(previous[label]['total'], test['total']) if previous && previous[label]
+
+  test['real (variance)'] = test['real'].reduce(0) { |acc, value|
+    acc + (test['real (avg)'] - value) ** 2
+  } / test['real'].length
+  test['total (variance)'] = test['total'].reduce(0) { |acc, value|
+    acc + (test['total (avg)'] - value) ** 2
+  } / test['total'].length
+
+  test['real (sd)'] = Math.sqrt(test['real (variance)'])
+  test['total (sd)'] = Math.sqrt(test['total (variance)'])
+end
+
+log_data.push({
+  'time' => now,
+  'results' => results,
+})
+File.open(log, 'w') { |f| f.write(log_data.to_yaml) }
+
+def print_table(rows)
+  rows.each do |row|
+    row.each.with_index do |cell, i|
+      width = rows.reduce(0) { |acc, row| row[i].length > acc ? row[i].length : acc }
+      if i.zero?
+        print align(cell, width)
+      else
+        print(' ' + align(cell, width))
+      end
+    end
+    puts
+  end
+end
+
+def align(str, width)
+  if str.respond_to?(:justify)
+    case str.justify
+    when :center
+      ('%*s%s%*s' % [
+        ((width - str.length) / 2.0).round,
+        '',
+        str,
+        ((width - str.length) / 2.0).round,
+        '',
+      ])[0...width]
+    when :left
+      '%-*s' % [width, str]
+    else
+      '%*s' % [width, str]
+    end
+  else
+    '%*s' % [width, str]
+  end
+end
+
+AnnotatedString = Struct.new(:length, :to_s, :justify)
+def center(str)
+  AnnotatedString.new(str.length, str, :center)
+end
+
+def float(x)
+  '%.5f' % x
+end
+
+def parens(x)
+  "(#{x})"
+end
+
+def trim(str)
+  str.sub(/0+\z/, '')
+end
+
+def maybe(value, default = '')
+  if value
+    yield value
+  else
+    default
+  end
+end
+
+puts "\n\nSummary of cpu time and (wall-clock time):\n"
+
+headers = [
+  [
+    '',
+    center('best'),
+    center('avg'),
+    center('sd'),
+    center('+/-'),
+    center('p'),
+    center('(best)'),
+    center('(avg)'),
+    center('(sd)'),
+    center('+/-'),
+    center('p'),
+  ]
+]
+rows = headers + results.map do |(label, data)|
+  [
+    label,
+    float(data['total (best)']),
+    float(data['total (avg)']),
+    float(data['total (sd)']),
+    maybe(data['total (+/-)'], center('?')) { |value| '[%+0.1f%%]' % value },
+    maybe(data['total (significance)']) { |value| value > 0 ? trim(float(value)) : '' },
+    parens(float(data['real (best)'])),
+    parens(float(data['real (avg)'])),
+    parens(float(data['real (sd)'])),
+    maybe(data['real (+/-)'], center('?')) { |value| '[%+0.1f%%]' % value },
+    maybe(data['real (significance)']) { |value| value > 0 ? trim(float(value)) : '' },
+  ]
+end
+print_table(rows)
diff --git a/tests/fixtures/integration/command-t/input/bin/benchmarks/watchman.rb b/tests/fixtures/integration/command-t/input/bin/benchmarks/watchman.rb
new file mode 100755 (executable)
index 0000000..8216b7a
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env ruby
+#
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+lib  = File.expand_path('../../ruby', File.dirname(__FILE__))
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+
+require 'command-t/ext'
+require 'benchmark'
+require 'json'
+require 'pathname'
+require 'socket'
+
+puts "Starting benchmark run (PID: #{Process.pid})"
+
+TEST_TIMES = 10
+
+Benchmark.bmbm do |b|
+  b.report('watchman JSON') do
+    TEST_TIMES.times do
+      sockname = JSON[%x{watchman get-sockname}]['sockname']
+      raise unless $?.exitstatus.zero?
+      UNIXSocket.open(sockname) do |s|
+        root = Pathname.new(ENV['PWD']).realpath
+        s.puts JSON.generate(['watch-list'])
+        if !JSON[s.gets]['roots'].include?(root)
+          # this path isn't being watched yet; try to set up watch
+          s.puts JSON.generate(['watch', root])
+
+          # root_restrict_files setting may prevent Watchman from working
+          raise if JSON[s.gets].has_key?('error')
+        end
+
+        s.puts JSON.generate(['query', root, {
+          'expression' => ['type', 'f'],
+          'fields'     => ['name'],
+        }])
+        paths = JSON[s.gets]
+
+        # could return error if watch is removed
+        raise if paths.has_key?('error')
+      end
+    end
+  end
+
+  b.report('watchman binary') do
+    TEST_TIMES.times do
+      sockname = CommandT::Watchman::Utils.load(
+        %x{watchman --output-encoding=bser get-sockname}
+      )['sockname']
+      raise unless $?.exitstatus.zero?
+
+      UNIXSocket.open(sockname) do |socket|
+        root = Pathname.new(ENV['PWD']).realpath.to_s
+        roots = CommandT::Watchman::Utils.query(['watch-list'], socket)['roots']
+        if !roots.include?(root)
+          # this path isn't being watched yet; try to set up watch
+          result = CommandT::Watchman::Utils.query(['watch', root], socket)
+
+          # root_restrict_files setting may prevent Watchman from working
+          raise if result.has_key?('error')
+        end
+
+        query = ['query', root, {
+          'expression' => ['type', 'f'],
+          'fields'     => ['name'],
+        }]
+        paths = CommandT::Watchman::Utils.query(query, socket)
+
+        # could return error if watch is removed
+        raise if paths.has_key?('error')
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/fixtures/bar/abc b/tests/fixtures/integration/command-t/input/fixtures/bar/abc
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/fixtures/bar/xyz b/tests/fixtures/integration/command-t/input/fixtures/bar/xyz
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/fixtures/baz b/tests/fixtures/integration/command-t/input/fixtures/baz
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/fixtures/bing b/tests/fixtures/integration/command-t/input/fixtures/bing
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/fixtures/foo/alpha/t1 b/tests/fixtures/integration/command-t/input/fixtures/foo/alpha/t1
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/fixtures/foo/alpha/t2 b/tests/fixtures/integration/command-t/input/fixtures/foo/alpha/t2
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/fixtures/foo/beta b/tests/fixtures/integration/command-t/input/fixtures/foo/beta
new file mode 100644 (file)
index 0000000..9c558e3
--- /dev/null
@@ -0,0 +1 @@
+.
diff --git a/tests/fixtures/integration/command-t/input/plugin/command-t.vim b/tests/fixtures/integration/command-t/input/plugin/command-t.vim
new file mode 100644 (file)
index 0000000..2e9b1a6
--- /dev/null
@@ -0,0 +1,20 @@
+" Copyright 2010-present Greg Hurrell. All rights reserved.
+" Licensed under the terms of the BSD 2-clause license.
+
+if exists('g:command_t_loaded') || &compatible
+  finish
+endif
+let g:command_t_loaded = 1
+
+" HACK: use both old and new during early development
+if has('patch-7-4-1829') && get(g:, 'CommandTEngine', 'mirkwood') ==? 'isengard'
+  call commandt#isengard#init()
+endif
+call commandt#mirkwood#init()
+finish
+
+if has('patch-7-4-1829') && get(g:, 'CommandTEngine', 'isengard') ==? 'isengard'
+  call commandt#isengard#init()
+else
+  call commandt#mirkwood#init()
+endif
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/bin/commandtd b/tests/fixtures/integration/command-t/input/ruby/command-t/bin/commandtd
new file mode 100755 (executable)
index 0000000..644d904
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/env ruby
+
+ERR_CHANNEL = STDERR.dup
+OUT_CHANNEL = STDOUT.dup
+
+STDERR.reopen('/dev/null', 'w')
+STDOUT.reopen('/dev/null', 'w')
+
+require 'ostruct'
+
+OPTIONS = OpenStruct.new
+
+def log(msg)
+  if OPTIONS.logfile
+    File.open(OPTIONS.logfile, 'a') { |f| f.puts "#{Time.now}: #{msg}"}
+  end
+  nil
+end
+
+require 'json'
+require 'optparse'
+
+begin
+  OptionParser.new do |opts|
+    opts.on('--vim-pid=PID') # Ignore: included only to make `ps` output more useful.
+    opts.on('--logfile=NAME') do |logfile|
+      OPTIONS.logfile = logfile
+    end
+  end.parse!
+rescue => e
+  log e
+end
+
+begin
+  require 'command-t'
+  require 'command-t/ext'
+rescue LoadError => e
+  load_path_modified = false
+  [
+    File.expand_path('../ext', File.dirname(__FILE__)),
+    File.expand_path('../lib', File.dirname(__FILE__))
+  ].each do |path|
+    if !$LOAD_PATH.include?(path)
+      $LOAD_PATH << path
+      load_path_modified = true
+    end
+  end
+  retry if load_path_modified
+
+  # TODO: show error here instructing to run install script
+  log e
+  exit 1
+end
+
+def read
+  line = STDIN.readline.chomp
+  input = JSON[line]
+  log "read: #{line}"
+  input
+rescue => e
+  log "read: #{e} reading line: #{line.inspect}"
+end
+
+def write(payload)
+  json = payload.to_json
+  OUT_CHANNEL.puts json
+  OUT_CHANNEL.flush
+  log "wrote: #{json}"
+end
+
+log "open for business running from #{__FILE__}"
+
+while true
+  log 'loop'
+  next unless input = read
+  if input['cd']
+    Dir.chdir(input['cd'])
+    log "changed directory to #{input['cd']}"
+    write({'cd' => input['cd']})
+  elsif input['match']
+    threads = CommandT::Util.processor_count
+    scanner = OpenStruct.new(:paths => ['commandt/foo', 'commandt/bar'])
+    matcher = CommandT::Matcher.new(scanner)
+    results = matcher.sorted_matches_for(input['match'], :threads => threads)
+    write({'results' => results})
+  else
+    write({'echo' => input})
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/command-t.gemspec b/tests/fixtures/integration/command-t/input/ruby/command-t/command-t.gemspec
new file mode 100644 (file)
index 0000000..4a1cf43
--- /dev/null
@@ -0,0 +1,32 @@
+Gem::Specification.new do |s|
+  s.name = 'command-t'
+
+  # see note in the Rakefile about how intermediate version numbers
+  # can break RubyGems
+  s.version = `git describe --abbrev=0`.chomp
+
+  s.authors = ['Greg Hurrell']
+  s.email = 'greg@hurrell.net'
+
+  s.files =
+    ['../../README.md', '../../LICENSE', '../../Gemfile', '../../Rakefile'] +
+    `git ls-files -z ./bin ./ext ./lib ../../doc`.split("\x0")
+
+  s.license = 'BSD'
+  s.require_paths = ['lib', 'ruby']
+  s.extensions = '/extconf.rb'
+
+  s.executables = ['commandtd']
+
+  s.has_rdoc = false
+  s.homepage = 'https://github.com/wincent/command-t'
+
+  s.summary = 'The Command-T plug-in for VIM.'
+
+  s.description = <<-EOS
+    Command-T provides a fast, intuitive mechanism for opening files with a
+    minimal number of keystrokes. Its full functionality is only available when
+    installed as a Vim plug-in, but it is also made available as a RubyGem so
+    that other applications can make use of its searching algorithm.
+  EOS
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/.gitignore b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/.gitignore
new file mode 100644 (file)
index 0000000..b65778f
--- /dev/null
@@ -0,0 +1,7 @@
+/ext.*
+!/ext.c
+!/ext.h
+/*.log
+/*.o
+/Makefile
+/metadata.rb
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/depend b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/depend
new file mode 100644 (file)
index 0000000..ddae9aa
--- /dev/null
@@ -0,0 +1,8 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+CFLAGS += -Wall -Wextra -Wno-unused-parameter
+
+ifdef DEBUG
+CFLAGS += -DDEBUG
+endif
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ext.c b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ext.c
new file mode 100644 (file)
index 0000000..4c3ffac
--- /dev/null
@@ -0,0 +1,40 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include "matcher.h"
+#include "watchman.h"
+
+VALUE mCommandT              = 0; // module CommandT
+VALUE cCommandTMatcher       = 0; // class CommandT::Matcher
+VALUE mCommandTWatchman      = 0; // module CommandT::Watchman
+VALUE mCommandTWatchmanUtils = 0; // module CommandT::Watchman::Utils
+
+VALUE CommandT_option_from_hash(const char *option, VALUE hash)
+{
+    VALUE key;
+    if (NIL_P(hash))
+        return Qnil;
+    key = ID2SYM(rb_intern(option));
+    if (rb_funcall(hash, rb_intern("has_key?"), 1, key) == Qtrue)
+        return rb_hash_aref(hash, key);
+    else
+        return Qnil;
+}
+
+void Init_ext()
+{
+    // module CommandT
+    mCommandT = rb_define_module("CommandT");
+
+    // class CommandT::Matcher
+    cCommandTMatcher = rb_define_class_under(mCommandT, "Matcher", rb_cObject);
+    rb_define_method(cCommandTMatcher, "initialize", CommandTMatcher_initialize, -1);
+    rb_define_method(cCommandTMatcher, "sorted_matches_for", CommandTMatcher_sorted_matches_for, -1);
+
+    // module CommandT::Watchman::Utils
+    mCommandTWatchman = rb_define_module_under(mCommandT, "Watchman");
+    mCommandTWatchmanUtils = rb_define_module_under(mCommandTWatchman, "Utils");
+    rb_define_singleton_method(mCommandTWatchmanUtils, "load", CommandTWatchmanUtils_load, 1);
+    rb_define_singleton_method(mCommandTWatchmanUtils, "dump", CommandTWatchmanUtils_dump, 1);
+    rb_define_singleton_method(mCommandTWatchmanUtils, "query", CommandTWatchmanUtils_query, 2);
+}
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ext.h b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ext.h
new file mode 100644 (file)
index 0000000..851aa73
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <ruby.h>
+
+extern VALUE mCommandT;              // module CommandT
+extern VALUE cCommandTMatcher;       // class CommandT::Matcher
+extern VALUE mCommandTWatchman;      // module CommandT::Watchman
+extern VALUE mCommandTWatchmanUtils; // module CommandT::Watchman::Utils
+
+// Encapsulates common pattern of checking for an option in an optional
+// options hash. The hash itself may be nil, but an exception will be
+// raised if it is not nil and not a hash.
+VALUE CommandT_option_from_hash(const char *option, VALUE hash);
+
+// Debugging macros.
+#define L(...) { \
+    fprintf(stdout, __VA_ARGS__); \
+    fflush(stdout); \
+} while (0)
+#define RUBY_INSPECT(obj) do { \
+    rb_funcall(rb_mKernel, rb_intern("p"), 1, obj); \
+    fflush(stdout); \
+} while (0)
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/extconf.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/extconf.rb
new file mode 100644 (file)
index 0000000..8603dad
--- /dev/null
@@ -0,0 +1,66 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'pathname'
+
+begin
+  require 'mkmf'
+rescue LoadError
+  puts <<-DOC.gsub(/^\s+/, '')
+    Unable to require "mkmf"; you may need to install Ruby development tools
+    (depending on your system, a "ruby-dev"/"ruby-devel" package or similar).
+    [exiting]
+  DOC
+  exit 1
+end
+
+def header(item)
+  unless find_header(item)
+    puts "couldn't find #{item} (required)"
+    exit 1
+  end
+end
+
+# mandatory headers
+header('float.h')
+header('ruby.h')
+header('stdlib.h')
+header('string.h')
+
+# optional headers (for CommandT::Watchman::Utils)
+if have_header('fcntl.h') &&
+  have_header('stdint.h') &&
+  have_header('sys/errno.h') &&
+  have_header('sys/socket.h')
+  RbConfig::MAKEFILE_CONFIG['DEFS'] ||= ''
+  RbConfig::MAKEFILE_CONFIG['DEFS'] += ' -DWATCHMAN_BUILD'
+
+  have_header('ruby/st.h') # >= 1.9; sets HAVE_RUBY_ST_H
+  have_header('st.h')      # 1.8; sets HAVE_ST_H
+end
+
+# optional
+if RbConfig::CONFIG['THREAD_MODEL'] == 'pthread'
+  have_library('pthread', 'pthread_create') # sets HAVE_PTHREAD_H if found
+end
+
+RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC']
+
+create_makefile('ext')
+
+# Create `metadata.rb`, which is used to diagnose installation problems.
+basedir = Pathname.new(__FILE__).dirname
+(basedir + 'metadata.rb').open('w') do |f|
+  f.puts <<-END.gsub(/^    /, '')
+    # This file was generated by #{(basedir + 'extconf.rb').to_s}
+    module CommandT
+      module Metadata
+        EXPECTED_RUBY_VERSION = #{RUBY_VERSION.inspect}
+        EXPECTED_RUBY_PATCHLEVEL = #{
+          defined?(RUBY_PATCHLEVEL) ? RUBY_PATCHLEVEL.inspect : nil.inspect
+        }
+        UNKNOWN = false
+      end
+    end
+  END
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/heap.c b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/heap.c
new file mode 100644 (file)
index 0000000..7a09677
--- /dev/null
@@ -0,0 +1,146 @@
+// Copyright 2016-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <stdlib.h> /* for free(), malloc(), NULL */
+
+#include "heap.h"
+
+#define HEAP_PARENT(index) ((index - 1) / 2)
+#define HEAP_LEFT(index) (2 * index + 1)
+#define HEAP_RIGHT(index) (2 * index + 2)
+
+/**
+ * Returns a new heap, or NULL on failure.
+ */
+heap_t *heap_new(long capacity, heap_compare_entries comparator) {
+    heap_t *heap = malloc(sizeof(heap_t));
+    if (!heap) {
+        return NULL;
+    }
+    heap->capacity = capacity;
+    heap->comparator = comparator;
+    heap->count = 0;
+
+    heap->entries = malloc(capacity * sizeof(void *));
+    if (!heap->entries) {
+        free(heap);
+        return NULL;
+    }
+    return heap;
+}
+
+/**
+ * Frees a previously created heap.
+ */
+void heap_free(heap_t *heap) {
+    free(heap->entries);
+    free(heap);
+}
+
+/**
+ * @internal
+ *
+ * Compare values at indices `a_idx` and `b_idx` using the heap's comparator
+ * function.
+ */
+int heap_compare(heap_t *heap, long a_idx, long b_idx) {
+    const void *a = heap->entries[a_idx];
+    const void *b = heap->entries[b_idx];
+    return heap->comparator(a, b);
+}
+
+/**
+ * @internal
+ *
+ * Returns 1 if the heap property holds (ie. parent < child).
+ */
+int heap_property(heap_t *heap, long parent_idx, long child_idx) {
+    return heap_compare(heap, parent_idx, child_idx) > 0;
+}
+
+/**
+ * @internal
+ *
+ * Swaps the values at indexes `a` and `b` within `heap`.
+ */
+void heap_swap(heap_t *heap, long a, long b) {
+    void *tmp = heap->entries[a];
+    heap->entries[a] = heap->entries[b];
+    heap->entries[b] = tmp;
+}
+
+/**
+ * Inserts `value` into `heap`.
+ */
+void heap_insert(heap_t *heap, void *value) {
+    long idx, parent_idx;
+
+    // If at capacity, ignore.
+    if (heap->count == heap->capacity) {
+        return;
+    }
+
+    // Insert into first empty slot.
+    idx = heap->count;
+    heap->entries[idx] = value;
+    heap->count++;
+
+    // Bubble upwards until heap property is restored.
+    parent_idx = HEAP_PARENT(idx);
+    while (idx && !heap_property(heap, parent_idx, idx)) {
+        heap_swap(heap, idx, parent_idx);
+        idx = parent_idx;
+        parent_idx = HEAP_PARENT(idx);
+    }
+}
+
+/**
+ * @internal
+ *
+ * Restores the heap property starting at `idx`.
+ */
+void heap_heapify(heap_t *heap, long idx) {
+    long left_idx = HEAP_LEFT(idx);
+    long right_idx = HEAP_RIGHT(idx);
+    long smallest_idx =
+        right_idx < heap->count ?
+
+        // Right (and therefore left) child exists.
+        (heap_compare(heap, left_idx, right_idx) > 0 ? left_idx : right_idx) :
+
+        left_idx < heap->count ?
+
+        // Only left child exists.
+        left_idx :
+
+        // No children exist.
+        idx;
+
+    if (
+        smallest_idx != idx &&
+        !heap_property(heap, idx, smallest_idx)
+    ) {
+        // Swap with smallest_idx child.
+        heap_swap(heap, idx, smallest_idx);
+        heap_heapify(heap, smallest_idx);
+    }
+}
+
+/**
+ * Extracts the minimum value from `heap`.
+ */
+void *heap_extract(heap_t *heap) {
+    void *extracted = NULL;
+    if (heap->count) {
+        // Grab root value.
+        extracted = heap->entries[0];
+
+        // Move last item to root.
+        heap->entries[0] = heap->entries[heap->count - 1];
+        heap->count--;
+
+        // Restore heap property.
+        heap_heapify(heap, 0);
+    }
+    return extracted;
+}
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/heap.h b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/heap.h
new file mode 100644 (file)
index 0000000..9dd0736
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright 2016-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+/**
+ * A fixed size min-heap implementation.
+ */
+
+typedef int (*heap_compare_entries)(const void *a, const void *b);
+
+typedef struct {
+    long count;
+    long capacity;
+    void **entries;
+    heap_compare_entries comparator;
+} heap_t;
+
+#define HEAP_PEEK(heap) (heap->entries[0])
+
+heap_t *heap_new(long capacity, heap_compare_entries comparator);
+void heap_free(heap_t *heap);
+void heap_insert(heap_t *heap, void *value);
+void *heap_extract(heap_t *heap);
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/match.c b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/match.c
new file mode 100644 (file)
index 0000000..7a28458
--- /dev/null
@@ -0,0 +1,243 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <float.h> /* for DBL_MAX */
+#include "match.h"
+#include "ext.h"
+#include "ruby_compat.h"
+
+#define UNSET_SCORE FLT_MAX
+
+// Use a struct to make passing params during recursion easier.
+typedef struct {
+    char    *haystack_p;            // Pointer to the path string to be searched.
+    long    haystack_len;           // Length of same.
+    char    *needle_p;              // Pointer to search string (needle).
+    long    needle_len;             // Length of same.
+    long    *rightmost_match_p;     // Rightmost match for each char in needle.
+    float   max_score_per_char;
+    int     always_show_dot_files;  // Boolean.
+    int     never_show_dot_files;   // Boolean.
+    int     case_sensitive;         // Boolean.
+    int     recurse;                // Boolean.
+    float   *memo;                  // Memoization.
+} matchinfo_t;
+
+float recursive_match(
+    matchinfo_t *m,    // Sharable meta-data.
+    long haystack_idx, // Where in the path string to start.
+    long needle_idx,   // Where in the needle string to start.
+    long last_idx,     // Location of last matched character.
+    float score        // Cumulative score so far.
+) {
+    long distance, i, j;
+    float *memoized = NULL;
+    float score_for_char;
+    float seen_score = 0;
+
+    // Iterate over needle.
+    for (i = needle_idx; i < m->needle_len; i++) {
+        // Iterate over (valid range of) haystack.
+        for (j = haystack_idx; j <= m->rightmost_match_p[i]; j++) {
+            char c, d;
+
+            // Do we have a memoized result we can return?
+            memoized = &m->memo[j * m->needle_len + i];
+            if (*memoized != UNSET_SCORE) {
+                return *memoized > seen_score ? *memoized : seen_score;
+            }
+            c = m->needle_p[i];
+            d = m->haystack_p[j];
+            if (d == '.') {
+                if (j == 0 || m->haystack_p[j - 1] == '/') { // This is a dot-file.
+                    int dot_search = c == '.'; // Searching for a dot.
+                    if (
+                        m->never_show_dot_files ||
+                        (!dot_search && !m->always_show_dot_files)
+                    ) {
+                        return *memoized = 0.0;
+                    }
+                }
+            } else if (d >= 'A' && d <= 'Z' && !m->case_sensitive) {
+                d += 'a' - 'A'; // Add 32 to downcase.
+            }
+
+            if (c == d) {
+                // Calculate score.
+                float sub_score = 0;
+                score_for_char = m->max_score_per_char;
+                distance = j - last_idx;
+
+                if (distance > 1) {
+                    float factor = 1.0;
+                    char last = m->haystack_p[j - 1];
+                    char curr = m->haystack_p[j]; // Case matters, so get again.
+                    if (last == '/') {
+                        factor = 0.9;
+                    } else if (
+                        last == '-' ||
+                        last == '_' ||
+                        last == ' ' ||
+                        (last >= '0' && last <= '9')
+                    ) {
+                        factor = 0.8;
+                    } else if (
+                        last >= 'a' && last <= 'z' &&
+                        curr >= 'A' && curr <= 'Z'
+                    ) {
+                        factor = 0.8;
+                    } else if (last == '.') {
+                        factor = 0.7;
+                    } else {
+                        // If no "special" chars behind char, factor diminishes
+                        // as distance from last matched char increases.
+                        factor = (1.0 / distance) * 0.75;
+                    }
+                    score_for_char *= factor;
+                }
+
+                if (j < m->rightmost_match_p[i] && m->recurse) {
+                    sub_score = recursive_match(m, j + 1, i, last_idx, score);
+                    if (sub_score > seen_score) {
+                        seen_score = sub_score;
+                    }
+                }
+                last_idx = j;
+                haystack_idx = last_idx + 1;
+                score += score_for_char;
+                *memoized = seen_score > score ? seen_score : score;
+                if (i == m->needle_len - 1) {
+                    // Whole string matched.
+                    return *memoized;
+                }
+                if (!m->recurse) {
+                    break;
+                }
+            }
+        }
+    }
+    return *memoized = score;
+}
+
+float calculate_match(
+    VALUE haystack,
+    VALUE needle,
+    VALUE case_sensitive,
+    VALUE always_show_dot_files,
+    VALUE never_show_dot_files,
+    VALUE recurse,
+    long needle_bitmask,
+    long *haystack_bitmask
+) {
+    matchinfo_t m;
+    long i;
+    float score             = 1.0;
+    int compute_bitmasks    = *haystack_bitmask == UNSET_BITMASK;
+    m.haystack_p            = RSTRING_PTR(haystack);
+    m.haystack_len          = RSTRING_LEN(haystack);
+    m.needle_p              = RSTRING_PTR(needle);
+    m.needle_len            = RSTRING_LEN(needle);
+    m.rightmost_match_p     = NULL;
+    m.max_score_per_char    = (1.0 / m.haystack_len + 1.0 / m.needle_len) / 2;
+    m.always_show_dot_files = always_show_dot_files == Qtrue;
+    m.never_show_dot_files  = never_show_dot_files == Qtrue;
+    m.case_sensitive        = (int)case_sensitive;
+    m.recurse               = recurse == Qtrue;
+
+    // Special case for zero-length search string.
+    if (m.needle_len == 0) {
+        // Filter out dot files.
+        if (m.never_show_dot_files || !m.always_show_dot_files) {
+            for (i = 0; i < m.haystack_len; i++) {
+                char c = m.haystack_p[i];
+                if (c == '.' && (i == 0 || m.haystack_p[i - 1] == '/')) {
+                    return 0.0;
+                }
+            }
+        }
+    } else {
+        long haystack_limit;
+        long memo_size;
+        long needle_idx;
+        long mask;
+        long rightmost_match_p[m.needle_len];
+
+        if (*haystack_bitmask != UNSET_BITMASK) {
+            if ((needle_bitmask & *haystack_bitmask) != needle_bitmask) {
+                return 0.0;
+            }
+        }
+
+        // Pre-scan string:
+        // - Bail if it can't match at all.
+        // - Record rightmost match for each character (prune search space).
+        // - Record bitmask for haystack to speed up future searches.
+        m.rightmost_match_p = rightmost_match_p;
+        needle_idx = m.needle_len - 1;
+        mask = 0;
+        for (i = m.haystack_len - 1; i >= 0; i--) {
+            char c = m.haystack_p[i];
+            char lower = c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c;
+            if (!m.case_sensitive) {
+                c = lower;
+            }
+            if (compute_bitmasks) {
+                mask |= (1 << (lower - 'a'));
+            }
+
+            if (needle_idx >= 0) {
+                char d = m.needle_p[needle_idx];
+                if (c == d) {
+                    rightmost_match_p[needle_idx] = i;
+                    needle_idx--;
+                }
+            }
+        }
+        if (compute_bitmasks) {
+            *haystack_bitmask = mask;
+        }
+        if (needle_idx != -1) {
+            return 0.0;
+        }
+
+        // Prepare for memoization.
+        haystack_limit = rightmost_match_p[m.needle_len - 1] + 1;
+        memo_size = m.needle_len * haystack_limit;
+        {
+            float memo[memo_size];
+            for (i = 0; i < memo_size; i++) {
+                memo[i] = UNSET_SCORE;
+            }
+            m.memo = memo;
+            score = recursive_match(&m, 0, 0, 0, 0.0);
+
+#ifdef DEBUG
+            fprintf(stdout, "   ");
+            for (i = 0; i < m.needle_len; i++) {
+                fprintf(stdout, "    %c   ", m.needle_p[i]);
+            }
+            fprintf(stdout, "\n");
+            for (i = 0; i < memo_size; i++) {
+                char formatted[8];
+                if (i % m.needle_len == 0) {
+                    long haystack_idx = i / m.needle_len;
+                    fprintf(stdout, "%c: ", m.haystack_p[haystack_idx]);
+                }
+                if (memo[i] == UNSET_SCORE) {
+                    snprintf(formatted, sizeof(formatted), "    -  ");
+                } else {
+                    snprintf(formatted, sizeof(formatted), " %-.4f", memo[i]);
+                }
+                fprintf(stdout, "%s", formatted);
+                if ((i + 1) % m.needle_len == 0) {
+                    fprintf(stdout, "\n");
+                } else {
+                    fprintf(stdout, " ");
+                }
+            }
+            fprintf(stdout, "Final score: %f\n\n", score);
+#endif
+        }
+    }
+    return score;
+}
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/match.h b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/match.h
new file mode 100644 (file)
index 0000000..073d1ee
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <ruby.h>
+
+#define UNSET_BITMASK (-1)
+
+// Struct for representing an individual match.
+typedef struct {
+    VALUE path;
+    long bitmask;
+    float score;
+} match_t;
+
+extern float calculate_match(
+    VALUE str,
+    VALUE needle,
+    VALUE case_sensitive,
+    VALUE always_show_dot_files,
+    VALUE never_show_dot_files,
+    VALUE recurse,
+    long needle_bitmask,
+    long *haystack_bitmask
+);
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/matcher.c b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/matcher.c
new file mode 100644 (file)
index 0000000..261b9ba
--- /dev/null
@@ -0,0 +1,382 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <stdlib.h>  /* for qsort() */
+#include <string.h>  /* for strncmp() */
+#include "match.h"
+#include "matcher.h"
+#include "heap.h"
+#include "ext.h"
+#include "ruby_compat.h"
+
+// order matters; we want this to be evaluated only after ruby.h
+#ifdef HAVE_PTHREAD_H
+#include <pthread.h> /* for pthread_create, pthread_join etc */
+#endif
+
+// Comparison function for use with qsort.
+int cmp_alpha(const void *a, const void *b)
+{
+    match_t a_match = *(match_t *)a;
+    match_t b_match = *(match_t *)b;
+    VALUE   a_str   = a_match.path;
+    VALUE   b_str   = b_match.path;
+    char    *a_p    = RSTRING_PTR(a_str);
+    long    a_len   = RSTRING_LEN(a_str);
+    char    *b_p    = RSTRING_PTR(b_str);
+    long    b_len   = RSTRING_LEN(b_str);
+    int     order   = 0;
+
+    if (a_len > b_len) {
+        order = strncmp(a_p, b_p, b_len);
+        if (order == 0)
+            order = 1; // shorter string (b) wins.
+    } else if (a_len < b_len) {
+        order = strncmp(a_p, b_p, a_len);
+        if (order == 0)
+            order = -1; // shorter string (a) wins.
+    } else {
+        order = strncmp(a_p, b_p, a_len);
+    }
+
+    return order;
+}
+
+// Comparison function for use with qsort.
+int cmp_score(const void *a, const void *b)
+{
+    match_t a_match = *(match_t *)a;
+    match_t b_match = *(match_t *)b;
+
+    if (a_match.score > b_match.score)
+        return -1; // a scores higher, a should appear sooner.
+    else if (a_match.score < b_match.score)
+        return 1;  // b scores higher, a should appear later.
+    else
+        return cmp_alpha(a, b);
+}
+
+VALUE CommandTMatcher_initialize(int argc, VALUE *argv, VALUE self)
+{
+    VALUE always_show_dot_files;
+    VALUE never_show_dot_files;
+    VALUE options;
+    VALUE scanner;
+
+    // Process arguments: 1 mandatory, 1 optional.
+    if (rb_scan_args(argc, argv, "11", &scanner, &options) == 1)
+        options = Qnil;
+    if (NIL_P(scanner))
+        rb_raise(rb_eArgError, "nil scanner");
+
+    rb_iv_set(self, "@scanner", scanner);
+
+    // Check optional options hash for overrides.
+    always_show_dot_files = CommandT_option_from_hash("always_show_dot_files", options);
+    never_show_dot_files = CommandT_option_from_hash("never_show_dot_files", options);
+
+    rb_iv_set(self, "@always_show_dot_files", always_show_dot_files);
+    rb_iv_set(self, "@never_show_dot_files", never_show_dot_files);
+
+    return Qnil;
+}
+
+typedef struct {
+    long thread_count;
+    long thread_index;
+    long case_sensitive;
+    long limit;
+    match_t *matches;
+    long path_count;
+    VALUE haystacks;
+    VALUE needle;
+    VALUE always_show_dot_files;
+    VALUE never_show_dot_files;
+    VALUE recurse;
+    long needle_bitmask;
+} thread_args_t;
+
+void *match_thread(void *thread_args)
+{
+    long i;
+    float score;
+    heap_t *heap = NULL;
+    thread_args_t *args = (thread_args_t *)thread_args;
+
+    if (args->limit) {
+        // Reserve one extra slot so that we can do an insert-then-extract even
+        // when "full" (effectively allows use of min-heap to maintain a
+        // top-"limit" list of items).
+        heap = heap_new(args->limit + 1, cmp_score);
+    }
+
+    for (
+        i = args->thread_index;
+        i < args->path_count;
+        i += args->thread_count
+    ) {
+        args->matches[i].path = RARRAY_PTR(args->haystacks)[i];
+        if (args->needle_bitmask == UNSET_BITMASK) {
+            args->matches[i].bitmask = UNSET_BITMASK;
+        }
+        args->matches[i].score = calculate_match(
+            args->matches[i].path,
+            args->needle,
+            args->case_sensitive,
+            args->always_show_dot_files,
+            args->never_show_dot_files,
+            args->recurse,
+            args->needle_bitmask,
+            &args->matches[i].bitmask
+        );
+        if (heap) {
+            if (heap->count == args->limit) {
+                score = ((match_t *)HEAP_PEEK(heap))->score;
+                if (args->matches[i].score >= score) {
+                    heap_insert(heap, &args->matches[i]);
+                    (void)heap_extract(heap);
+                }
+            } else {
+                heap_insert(heap, &args->matches[i]);
+            }
+        }
+    }
+
+    return heap;
+}
+
+long calculate_bitmask(VALUE string) {
+    char *str = RSTRING_PTR(string);
+    long len = RSTRING_LEN(string);
+    long i;
+    long mask = 0;
+    for (i = 0; i < len; i++) {
+        if (str[i] >= 'a' && str[i] <= 'z') {
+            mask |= (1 << (str[i] - 'a'));
+        } else if (str[i] >= 'A' && str[i] <= 'Z') {
+            mask |= (1 << (str[i] - 'A'));
+        }
+    }
+    return mask;
+}
+
+VALUE CommandTMatcher_sorted_matches_for(int argc, VALUE *argv, VALUE self)
+{
+    long i, j, limit, path_count, thread_count;
+#ifdef HAVE_PTHREAD_H
+    long err;
+    pthread_t *threads;
+#endif
+    long needle_bitmask = UNSET_BITMASK;
+    long heap_matches_count;
+    int use_heap;
+    int sort;
+    match_t *matches;
+    match_t *heap_matches = NULL;
+    heap_t *heap;
+    thread_args_t *thread_args;
+    VALUE always_show_dot_files;
+    VALUE case_sensitive;
+    VALUE recurse;
+    VALUE ignore_spaces;
+    VALUE limit_option;
+    VALUE needle;
+    VALUE never_show_dot_files;
+    VALUE new_paths_object_id;
+    VALUE options;
+    VALUE paths;
+    VALUE paths_object_id;
+    VALUE results;
+    VALUE scanner;
+    VALUE sort_option;
+    VALUE threads_option;
+    VALUE wrapped_matches;
+
+    // Process arguments: 1 mandatory, 1 optional.
+    if (rb_scan_args(argc, argv, "11", &needle, &options) == 1)
+        options = Qnil;
+    if (NIL_P(needle))
+        rb_raise(rb_eArgError, "nil needle");
+
+    // Check optional options hash for overrides.
+    case_sensitive = CommandT_option_from_hash("case_sensitive", options);
+    limit_option = CommandT_option_from_hash("limit", options);
+    threads_option = CommandT_option_from_hash("threads", options);
+    sort_option = CommandT_option_from_hash("sort", options);
+    ignore_spaces = CommandT_option_from_hash("ignore_spaces", options);
+    always_show_dot_files = rb_iv_get(self, "@always_show_dot_files");
+    never_show_dot_files = rb_iv_get(self, "@never_show_dot_files");
+    recurse = CommandT_option_from_hash("recurse", options);
+
+    limit = NIL_P(limit_option) ? 15 : NUM2LONG(limit_option);
+    sort = NIL_P(sort_option) || sort_option == Qtrue;
+    use_heap = limit && sort;
+    heap_matches_count = 0;
+
+    needle = StringValue(needle);
+    if (case_sensitive != Qtrue)
+        needle = rb_funcall(needle, rb_intern("downcase"), 0);
+
+    if (ignore_spaces == Qtrue)
+        needle = rb_funcall(needle, rb_intern("delete"), 1, rb_str_new2(" "));
+
+    // Get unsorted matches.
+    scanner = rb_iv_get(self, "@scanner");
+    paths = rb_funcall(scanner, rb_intern("paths"), 0);
+    path_count = RARRAY_LEN(paths);
+
+    // Cached C data, not visible to Ruby layer.
+    paths_object_id = rb_ivar_get(self, rb_intern("paths_object_id"));
+    new_paths_object_id = rb_funcall(paths, rb_intern("object_id"), 0);
+    rb_ivar_set(self, rb_intern("paths_object_id"), new_paths_object_id);
+    if (
+        NIL_P(paths_object_id) ||
+        NUM2LONG(new_paths_object_id) != NUM2LONG(paths_object_id)
+    ) {
+        // `paths` changed, need to replace matches array.
+        paths_object_id = new_paths_object_id;
+        matches = malloc(path_count * sizeof(match_t));
+        if (!matches) {
+            rb_raise(rb_eNoMemError, "memory allocation failed");
+        }
+        wrapped_matches = Data_Wrap_Struct(
+            rb_cObject,
+            0,
+            free,
+            matches
+        );
+        rb_ivar_set(self, rb_intern("matches"), wrapped_matches);
+    } else {
+        // Get existing array.
+        Data_Get_Struct(
+            rb_ivar_get(self, rb_intern("matches")),
+            match_t,
+            matches
+        );
+
+        // Will compare against previously computed haystack bitmasks.
+        needle_bitmask = calculate_bitmask(needle);
+    }
+
+    thread_count = NIL_P(threads_option) ? 1 : NUM2LONG(threads_option);
+    if (use_heap) {
+        heap_matches = malloc(thread_count * limit * sizeof(match_t));
+        if (!heap_matches) {
+            rb_raise(rb_eNoMemError, "memory allocation failed");
+        }
+    }
+
+#ifdef HAVE_PTHREAD_H
+#define THREAD_THRESHOLD 1000 /* avoid the overhead of threading when search space is small */
+    if (path_count < THREAD_THRESHOLD) {
+        thread_count = 1;
+    }
+    threads = malloc(sizeof(pthread_t) * thread_count);
+    if (!threads)
+        rb_raise(rb_eNoMemError, "memory allocation failed");
+#endif
+
+    thread_args = malloc(sizeof(thread_args_t) * thread_count);
+    if (!thread_args)
+        rb_raise(rb_eNoMemError, "memory allocation failed");
+    for (i = 0; i < thread_count; i++) {
+        thread_args[i].thread_count = thread_count;
+        thread_args[i].thread_index = i;
+        thread_args[i].case_sensitive = case_sensitive == Qtrue;
+        thread_args[i].matches = matches;
+        thread_args[i].limit = use_heap ? limit : 0;
+        thread_args[i].path_count = path_count;
+        thread_args[i].haystacks = paths;
+        thread_args[i].needle = needle;
+        thread_args[i].always_show_dot_files = always_show_dot_files;
+        thread_args[i].never_show_dot_files = never_show_dot_files;
+        thread_args[i].recurse = recurse;
+        thread_args[i].needle_bitmask = needle_bitmask;
+
+#ifdef HAVE_PTHREAD_H
+        if (i == thread_count - 1) {
+#endif
+            // For the last "worker", we'll just use the main thread.
+            heap = match_thread(&thread_args[i]);
+            if (heap) {
+                for (j = 0; j < heap->count; j++) {
+                    heap_matches[heap_matches_count++] = *(match_t *)heap->entries[j];
+                }
+                heap_free(heap);
+            }
+#ifdef HAVE_PTHREAD_H
+        } else {
+            err = pthread_create(&threads[i], NULL, match_thread, (void *)&thread_args[i]);
+            if (err != 0) {
+                rb_raise(rb_eSystemCallError, "pthread_create() failure (%d)", (int)err);
+            }
+        }
+#endif
+    }
+
+#ifdef HAVE_PTHREAD_H
+    for (i = 0; i < thread_count - 1; i++) {
+        err = pthread_join(threads[i], (void **)&heap);
+        if (err != 0) {
+            rb_raise(rb_eSystemCallError, "pthread_join() failure (%d)", (int)err);
+        }
+        if (heap) {
+            for (j = 0; j < heap->count; j++) {
+                heap_matches[heap_matches_count++] = *(match_t *)heap->entries[j];
+            }
+            heap_free(heap);
+        }
+    }
+    free(threads);
+#endif
+
+    if (sort) {
+        if (
+            RSTRING_LEN(needle) == 0 ||
+            (RSTRING_LEN(needle) == 1 && RSTRING_PTR(needle)[0] == '.')
+        ) {
+            // Alphabetic order if search string is only "" or "."
+            // TODO: make those semantics fully apply to heap case as well
+            // (they don't because the heap itself calls cmp_score, which means
+            // that the items which stay in the top [limit] may (will) be
+            // different).
+            qsort(
+                use_heap ? heap_matches : matches,
+                use_heap ? heap_matches_count : path_count,
+                sizeof(match_t),
+                cmp_alpha
+            );
+        } else {
+            qsort(
+                use_heap ? heap_matches : matches,
+                use_heap ? heap_matches_count : path_count,
+                sizeof(match_t),
+                cmp_score
+            );
+        }
+    }
+
+    results = rb_ary_new();
+    if (limit == 0)
+        limit = path_count;
+    for (
+        i = 0;
+        i < (use_heap ? heap_matches_count : path_count) && limit > 0;
+        i++
+    ) {
+        if ((use_heap ? heap_matches : matches)[i].score > 0.0) {
+            rb_funcall(
+                results,
+                rb_intern("push"),
+                1,
+                (use_heap ? heap_matches : matches)[i].path
+            );
+            limit--;
+        }
+    }
+
+    if (use_heap) {
+        free(heap_matches);
+    }
+    return results;
+}
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/matcher.h b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/matcher.h
new file mode 100644 (file)
index 0000000..c2a6990
--- /dev/null
@@ -0,0 +1,7 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <ruby.h>
+
+extern VALUE CommandTMatcher_initialize(int argc, VALUE *argv, VALUE self);
+extern VALUE CommandTMatcher_sorted_matches_for(int argc, VALUE *argv, VALUE self);
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ruby_compat.h b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/ruby_compat.h
new file mode 100644 (file)
index 0000000..dadcdea
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright 2010-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <ruby.h>
+
+// for compatibility with older versions of Ruby which don't declare RSTRING_PTR
+#ifndef RSTRING_PTR
+#define RSTRING_PTR(s) (RSTRING(s)->ptr)
+#endif
+
+// for compatibility with older versions of Ruby which don't declare RSTRING_LEN
+#ifndef RSTRING_LEN
+#define RSTRING_LEN(s) (RSTRING(s)->len)
+#endif
+
+// for compatibility with older versions of Ruby which don't declare RARRAY_PTR
+#ifndef RARRAY_PTR
+#define RARRAY_PTR(a) (RARRAY(a)->ptr)
+#endif
+
+// for compatibility with older versions of Ruby which don't declare RARRAY_LEN
+#ifndef RARRAY_LEN
+#define RARRAY_LEN(a) (RARRAY(a)->len)
+#endif
+
+// for compatibility with older versions of Ruby which don't declare RFLOAT_VALUE
+#ifndef RFLOAT_VALUE
+#define RFLOAT_VALUE(f) (RFLOAT(f)->value)
+#endif
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/watchman.c b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/watchman.c
new file mode 100644 (file)
index 0000000..131ffda
--- /dev/null
@@ -0,0 +1,660 @@
+// Copyright 2014-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include "watchman.h"
+
+#ifdef WATCHMAN_BUILD
+
+#if defined(HAVE_RUBY_ST_H)
+#include <ruby/st.h>
+#elif defined(HAVE_ST_H)
+#include <st.h>
+#else
+#error no st.h header found
+#endif
+
+#include <stdint.h>     /* for uint8_t */
+#include <fcntl.h>      /* for fcntl() */
+#include <sys/errno.h>  /* for errno */
+#include <sys/socket.h> /* for recv(), MSG_PEEK */
+
+typedef struct {
+    uint8_t *data;  // payload
+    size_t cap;     // total capacity
+    size_t len;     // current length
+} watchman_t;
+
+// Forward declarations:
+VALUE watchman_load(char **ptr, char *end);
+void watchman_dump(watchman_t *w, VALUE serializable);
+
+#define WATCHMAN_DEFAULT_STORAGE 4096
+
+#define WATCHMAN_BINARY_MARKER   "\x00\x01"
+#define WATCHMAN_ARRAY_MARKER    0x00
+#define WATCHMAN_HASH_MARKER     0x01
+#define WATCHMAN_STRING_MARKER   0x02
+#define WATCHMAN_INT8_MARKER     0x03
+#define WATCHMAN_INT16_MARKER    0x04
+#define WATCHMAN_INT32_MARKER    0x05
+#define WATCHMAN_INT64_MARKER    0x06
+#define WATCHMAN_FLOAT_MARKER    0x07
+#define WATCHMAN_TRUE            0x08
+#define WATCHMAN_FALSE           0x09
+#define WATCHMAN_NIL             0x0a
+#define WATCHMAN_TEMPLATE_MARKER 0x0b
+#define WATCHMAN_SKIP_MARKER     0x0c
+
+#define WATCHMAN_HEADER \
+        WATCHMAN_BINARY_MARKER \
+        "\x06" \
+        "\x00\x00\x00\x00\x00\x00\x00\x00"
+
+static const char watchman_array_marker  = WATCHMAN_ARRAY_MARKER;
+static const char watchman_hash_marker   = WATCHMAN_HASH_MARKER;
+static const char watchman_string_marker = WATCHMAN_STRING_MARKER;
+static const char watchman_true          = WATCHMAN_TRUE;
+static const char watchman_false         = WATCHMAN_FALSE;
+static const char watchman_nil           = WATCHMAN_NIL;
+
+/**
+ * Appends `len` bytes, starting at `data`, to the watchman_t struct `w`
+ *
+ * Will attempt to reallocate the underlying storage if it is not sufficient.
+ */
+void watchman_append(watchman_t *w, const char *data, size_t len) {
+    if (w->len + len > w->cap) {
+        w->cap += w->len + WATCHMAN_DEFAULT_STORAGE;
+        REALLOC_N(w->data, uint8_t, w->cap);
+    }
+    memcpy(w->data + w->len, data, len);
+    w->len += len;
+}
+
+/**
+ * Allocate a new watchman_t struct
+ *
+ * The struct has a small amount of extra capacity preallocated, and a blank
+ * header that can be filled in later to describe the PDU.
+ */
+watchman_t *watchman_init() {
+    watchman_t *w = ALLOC(watchman_t);
+    w->cap = WATCHMAN_DEFAULT_STORAGE;
+    w->len = 0;
+    w->data = ALLOC_N(uint8_t, WATCHMAN_DEFAULT_STORAGE);
+
+    watchman_append(w, WATCHMAN_HEADER, sizeof(WATCHMAN_HEADER) - 1);
+    return w;
+}
+
+/**
+ * Free a watchman_t struct `w` that was previously allocated with
+ * `watchman_init`
+ */
+void watchman_free(watchman_t *w) {
+    xfree(w->data);
+    xfree(w);
+}
+
+/**
+ * Encodes and appends the integer `num` to `w`
+ */
+void watchman_dump_int(watchman_t *w, int64_t num) {
+    char encoded[1 + sizeof(int64_t)];
+
+    if (num == (int8_t)num) {
+        encoded[0] = WATCHMAN_INT8_MARKER;
+        encoded[1] = (int8_t)num;
+        watchman_append(w, encoded, 1 + sizeof(int8_t));
+    } else if (num == (int16_t)num) {
+        encoded[0] = WATCHMAN_INT16_MARKER;
+        *(int16_t *)(encoded + 1) = (int16_t)num;
+        watchman_append(w, encoded, 1 + sizeof(int16_t));
+    } else if (num == (int32_t)num) {
+        encoded[0] = WATCHMAN_INT32_MARKER;
+        *(int32_t *)(encoded + 1) = (int32_t)num;
+        watchman_append(w, encoded, 1 + sizeof(int32_t));
+    } else {
+        encoded[0] = WATCHMAN_INT64_MARKER;
+        *(int64_t *)(encoded + 1) = (int64_t)num;
+        watchman_append(w, encoded, 1 + sizeof(int64_t));
+    }
+}
+
+/**
+ * Encodes and appends the string `string` to `w`
+ */
+void watchman_dump_string(watchman_t *w, VALUE string) {
+    watchman_append(w, &watchman_string_marker, sizeof(watchman_string_marker));
+    watchman_dump_int(w, RSTRING_LEN(string));
+    watchman_append(w, RSTRING_PTR(string), RSTRING_LEN(string));
+}
+
+/**
+ * Encodes and appends the double `num` to `w`
+ */
+void watchman_dump_double(watchman_t *w, double num) {
+    char encoded[1 + sizeof(double)];
+    encoded[0] = WATCHMAN_FLOAT_MARKER;
+    *(double *)(encoded + 1) = num;
+    watchman_append(w, encoded, sizeof(encoded));
+}
+
+/**
+ * Encodes and appends the array `array` to `w`
+ */
+void watchman_dump_array(watchman_t *w, VALUE array) {
+    long i;
+    watchman_append(w, &watchman_array_marker, sizeof(watchman_array_marker));
+    watchman_dump_int(w, RARRAY_LEN(array));
+    for (i = 0; i < RARRAY_LEN(array); i++) {
+        watchman_dump(w, rb_ary_entry(array, i));
+    }
+}
+
+/**
+ * Helper method that encodes and appends a key/value pair (`key`, `value`) from
+ * a hash to the watchman_t struct passed in via `data`
+ */
+int watchman_dump_hash_iterator(VALUE key, VALUE value, VALUE data) {
+    watchman_t *w = (watchman_t *)data;
+    watchman_dump_string(w, StringValue(key));
+    watchman_dump(w, value);
+    return ST_CONTINUE;
+}
+
+/**
+ * Encodes and appends the hash `hash` to `w`
+ */
+void watchman_dump_hash(watchman_t *w, VALUE hash) {
+    watchman_append(w, &watchman_hash_marker, sizeof(watchman_hash_marker));
+    watchman_dump_int(w, RHASH_SIZE(hash));
+    rb_hash_foreach(hash, watchman_dump_hash_iterator, (VALUE)w);
+}
+
+/**
+ * Encodes and appends the serialized Ruby object `serializable` to `w`
+ *
+ * Examples of serializable objects include arrays, hashes, strings, numbers
+ * (integers, floats), booleans, and nil.
+ */
+void watchman_dump(watchman_t *w, VALUE serializable) {
+    switch (TYPE(serializable)) {
+        case T_ARRAY:
+            return watchman_dump_array(w, serializable);
+        case T_HASH:
+            return watchman_dump_hash(w, serializable);
+        case T_STRING:
+            return watchman_dump_string(w, serializable);
+        case T_FIXNUM: // up to 63 bits
+            return watchman_dump_int(w, FIX2LONG(serializable));
+        case T_BIGNUM:
+            return watchman_dump_int(w, NUM2LL(serializable));
+        case T_FLOAT:
+            return watchman_dump_double(w, NUM2DBL(serializable));
+        case T_TRUE:
+            return watchman_append(w, &watchman_true, sizeof(watchman_true));
+        case T_FALSE:
+            return watchman_append(w, &watchman_false, sizeof(watchman_false));
+        case T_NIL:
+            return watchman_append(w, &watchman_nil, sizeof(watchman_nil));
+        default:
+            rb_raise(rb_eTypeError, "unsupported type");
+    }
+}
+
+/**
+ * Extract and return the int encoded at `ptr`
+ *
+ * Moves `ptr` past the extracted int.
+ *
+ * Will raise an ArgumentError if extracting the int would take us beyond the
+ * end of the buffer indicated by `end`, or if there is no int encoded at `ptr`.
+ *
+ * @returns The extracted int
+ */
+int64_t watchman_load_int(char **ptr, char *end) {
+    char *val_ptr = *ptr + sizeof(int8_t);
+    int64_t val = 0;
+
+    if (val_ptr >= end) {
+        rb_raise(rb_eArgError, "insufficient int storage");
+    }
+
+    switch (*ptr[0]) {
+        case WATCHMAN_INT8_MARKER:
+            if (val_ptr + sizeof(int8_t) > end) {
+                rb_raise(rb_eArgError, "overrun extracting int8_t");
+            }
+            val = *(int8_t *)val_ptr;
+            *ptr = val_ptr + sizeof(int8_t);
+            break;
+        case WATCHMAN_INT16_MARKER:
+            if (val_ptr + sizeof(int16_t) > end) {
+                rb_raise(rb_eArgError, "overrun extracting int16_t");
+            }
+            val = *(int16_t *)val_ptr;
+            *ptr = val_ptr + sizeof(int16_t);
+            break;
+        case WATCHMAN_INT32_MARKER:
+            if (val_ptr + sizeof(int32_t) > end) {
+                rb_raise(rb_eArgError, "overrun extracting int32_t");
+            }
+            val = *(int32_t *)val_ptr;
+            *ptr = val_ptr + sizeof(int32_t);
+            break;
+        case WATCHMAN_INT64_MARKER:
+            if (val_ptr + sizeof(int64_t) > end) {
+                rb_raise(rb_eArgError, "overrun extracting int64_t");
+            }
+            val = *(int64_t *)val_ptr;
+            *ptr = val_ptr + sizeof(int64_t);
+            break;
+        default:
+            rb_raise(rb_eArgError, "bad integer marker 0x%02x", (unsigned int)*ptr[0]);
+            break;
+    }
+
+    return val;
+}
+
+/**
+ * Reads and returns a string encoded in the Watchman binary protocol format,
+ * starting at `ptr` and finishing at or before `end`
+ */
+VALUE watchman_load_string(char **ptr, char *end) {
+    int64_t len;
+    VALUE string;
+    if (*ptr >= end) {
+        rb_raise(rb_eArgError, "unexpected end of input");
+    }
+
+    if (*ptr[0] != WATCHMAN_STRING_MARKER) {
+        rb_raise(rb_eArgError, "not a number");
+    }
+
+    *ptr += sizeof(int8_t);
+    if (*ptr >= end) {
+        rb_raise(rb_eArgError, "invalid string header");
+    }
+
+    len = watchman_load_int(ptr, end);
+    if (len == 0) { // special case for zero-length strings
+        return rb_str_new2("");
+    } else if (*ptr + len > end) {
+        rb_raise(rb_eArgError, "insufficient string storage");
+    }
+
+    string = rb_str_new(*ptr, len);
+    *ptr += len;
+    return string;
+}
+
+/**
+ * Reads and returns a double encoded in the Watchman binary protocol format,
+ * starting at `ptr` and finishing at or before `end`
+ */
+double watchman_load_double(char **ptr, char *end) {
+    double val;
+    *ptr += sizeof(int8_t); // caller has already verified the marker
+    if (*ptr + sizeof(double) > end) {
+        rb_raise(rb_eArgError, "insufficient double storage");
+    }
+    val = *(double *)*ptr;
+    *ptr += sizeof(double);
+    return val;
+}
+
+/**
+ * Helper method which returns length of the array encoded in the Watchman
+ * binary protocol format, starting at `ptr` and finishing at or before `end`
+ */
+int64_t watchman_load_array_header(char **ptr, char *end) {
+    if (*ptr >= end) {
+        rb_raise(rb_eArgError, "unexpected end of input");
+    }
+
+    // verify and consume marker
+    if (*ptr[0] != WATCHMAN_ARRAY_MARKER) {
+        rb_raise(rb_eArgError, "not an array");
+    }
+    *ptr += sizeof(int8_t);
+
+    // expect a count
+    if (*ptr + sizeof(int8_t) * 2 > end) {
+        rb_raise(rb_eArgError, "incomplete array header");
+    }
+    return watchman_load_int(ptr, end);
+}
+
+/**
+ * Reads and returns an array encoded in the Watchman binary protocol format,
+ * starting at `ptr` and finishing at or before `end`
+ */
+VALUE watchman_load_array(char **ptr, char *end) {
+    int64_t count, i;
+    VALUE array;
+
+    count = watchman_load_array_header(ptr, end);
+    array = rb_ary_new2(count);
+
+    for (i = 0; i < count; i++) {
+        rb_ary_push(array, watchman_load(ptr, end));
+    }
+
+    return array;
+}
+
+/**
+ * Reads and returns a hash encoded in the Watchman binary protocol format,
+ * starting at `ptr` and finishing at or before `end`
+ */
+VALUE watchman_load_hash(char **ptr, char *end) {
+    int64_t count, i;
+    VALUE hash, key, value;
+
+    *ptr += sizeof(int8_t); // caller has already verified the marker
+
+    // expect a count
+    if (*ptr + sizeof(int8_t) * 2 > end) {
+        rb_raise(rb_eArgError, "incomplete hash header");
+    }
+    count = watchman_load_int(ptr, end);
+
+    hash = rb_hash_new();
+
+    for (i = 0; i < count; i++) {
+        key = watchman_load_string(ptr, end);
+        value = watchman_load(ptr, end);
+        rb_hash_aset(hash, key, value);
+    }
+
+    return hash;
+}
+
+/**
+ * Reads and returns a templated array encoded in the Watchman binary protocol
+ * format, starting at `ptr` and finishing at or before `end`
+ *
+ * Templated arrays are arrays of hashes which have repetitive key information
+ * pulled out into a separate "headers" prefix.
+ *
+ * @see https://github.com/facebook/watchman/blob/master/website/_docs/BSER.markdown
+ */
+VALUE watchman_load_template(char **ptr, char *end) {
+    int64_t header_items_count, i, row_count;
+    VALUE array, hash, header, key, value;
+
+    *ptr += sizeof(int8_t); // caller has already verified the marker
+
+    // process template header array
+    header_items_count = watchman_load_array_header(ptr, end);
+    header = rb_ary_new2(header_items_count);
+    for (i = 0; i < header_items_count; i++) {
+        rb_ary_push(header, watchman_load_string(ptr, end));
+    }
+
+    // process row items
+    row_count = watchman_load_int(ptr, end);
+    array = rb_ary_new2(header_items_count);
+    while (row_count--) {
+        hash = rb_hash_new();
+        for (i = 0; i < header_items_count; i++) {
+            if (*ptr >= end) {
+                rb_raise(rb_eArgError, "unexpected end of input");
+            }
+
+            if (*ptr[0] == WATCHMAN_SKIP_MARKER) {
+                *ptr += sizeof(uint8_t);
+            } else {
+                value = watchman_load(ptr, end);
+                key = rb_ary_entry(header, i);
+                rb_hash_aset(hash, key, value);
+            }
+        }
+        rb_ary_push(array, hash);
+    }
+    return array;
+}
+
+/**
+ * Reads and returns an object encoded in the Watchman binary protocol format,
+ * starting at `ptr` and finishing at or before `end`
+ */
+VALUE watchman_load(char **ptr, char *end) {
+    if (*ptr >= end) {
+        rb_raise(rb_eArgError, "unexpected end of input");
+    }
+
+    switch (*ptr[0]) {
+        case WATCHMAN_ARRAY_MARKER:
+            return watchman_load_array(ptr, end);
+        case WATCHMAN_HASH_MARKER:
+            return watchman_load_hash(ptr, end);
+        case WATCHMAN_STRING_MARKER:
+            return watchman_load_string(ptr, end);
+        case WATCHMAN_INT8_MARKER:
+        case WATCHMAN_INT16_MARKER:
+        case WATCHMAN_INT32_MARKER:
+        case WATCHMAN_INT64_MARKER:
+            return LL2NUM(watchman_load_int(ptr, end));
+        case WATCHMAN_FLOAT_MARKER:
+            return rb_float_new(watchman_load_double(ptr, end));
+        case WATCHMAN_TRUE:
+            *ptr += 1;
+            return Qtrue;
+        case WATCHMAN_FALSE:
+            *ptr += 1;
+            return Qfalse;
+        case WATCHMAN_NIL:
+            *ptr += 1;
+            return Qnil;
+        case WATCHMAN_TEMPLATE_MARKER:
+            return watchman_load_template(ptr, end);
+        default:
+            rb_raise(rb_eTypeError, "unsupported type");
+    }
+
+    return Qnil; // keep the compiler happy
+}
+
+/**
+ * CommandT::Watchman::Utils.load(serialized)
+ *
+ * Converts the binary object, `serialized`, from the Watchman binary protocol
+ * format into a normal Ruby object.
+ */
+VALUE CommandTWatchmanUtils_load(VALUE self, VALUE serialized) {
+    char *ptr, *end;
+    long len;
+    uint64_t payload_size;
+    VALUE loaded;
+    serialized = StringValue(serialized);
+    len = RSTRING_LEN(serialized);
+    ptr = RSTRING_PTR(serialized);
+    end = ptr + len;
+
+    // expect at least the binary marker and a int8_t length counter
+    if ((size_t)len < sizeof(WATCHMAN_BINARY_MARKER) - 1 + sizeof(int8_t) * 2) {
+        rb_raise(rb_eArgError, "undersized header");
+    }
+
+    if (memcmp(ptr, WATCHMAN_BINARY_MARKER, sizeof(WATCHMAN_BINARY_MARKER) - 1)) {
+        rb_raise(rb_eArgError, "missing binary marker");
+    }
+
+    // get size marker
+    ptr += sizeof(WATCHMAN_BINARY_MARKER) - 1;
+    payload_size = watchman_load_int(&ptr, end);
+    if (!payload_size) {
+        rb_raise(rb_eArgError, "empty payload");
+    }
+
+    // sanity check length
+    if (ptr + payload_size != end) {
+        rb_raise(
+            rb_eArgError,
+            "payload size mismatch (%lu)",
+            (unsigned long)(end - (ptr + payload_size))
+        );
+    }
+
+    loaded = watchman_load(&ptr, end);
+
+    // one more sanity check
+    if (ptr != end) {
+        rb_raise(
+            rb_eArgError,
+            "payload termination mismatch (%lu)",
+            (unsigned long)(end - ptr)
+        );
+    }
+
+    return loaded;
+}
+
+/**
+ * CommandT::Watchman::Utils.dump(serializable)
+ *
+ * Converts the Ruby object, `serializable`, into a binary string in the
+ * Watchman binary protocol format.
+ *
+ * Examples of serializable objects include arrays, hashes, strings, numbers
+ * (integers, floats), booleans, and nil.
+ */
+VALUE CommandTWatchmanUtils_dump(VALUE self, VALUE serializable) {
+    uint64_t *len;
+    VALUE serialized;
+    watchman_t *w = watchman_init();
+    watchman_dump(w, serializable);
+
+    // update header with final length information
+    len = (uint64_t *)(w->data + sizeof(WATCHMAN_HEADER) - sizeof(uint64_t) - 1);
+    *len = w->len - sizeof(WATCHMAN_HEADER) + 1;
+
+    // prepare final return value
+    serialized = rb_str_buf_new(w->len);
+    rb_str_buf_cat(serialized, (const char*)w->data, w->len);
+    watchman_free(w);
+    return serialized;
+}
+
+/**
+ * Helper method for raising a SystemCallError wrapping a lower-level error code
+ * coming from the `errno` global variable.
+ */
+void watchman_raise_system_call_error(int number) {
+    VALUE error = INT2FIX(number);
+    rb_exc_raise(rb_class_new_instance(1, &error, rb_eSystemCallError));
+}
+
+// How far we have to look to figure out the size of the PDU header
+#define WATCHMAN_SNIFF_BUFFER_SIZE sizeof(WATCHMAN_BINARY_MARKER) - 1 + sizeof(int8_t)
+
+// How far we have to peek, at most, to figure out the size of the PDU itself
+#define WATCHMAN_PEEK_BUFFER_SIZE \
+    sizeof(WATCHMAN_BINARY_MARKER) - 1 + \
+    sizeof(WATCHMAN_INT64_MARKER) + \
+    sizeof(int64_t)
+
+/**
+ * CommandT::Watchman::Utils.query(query, socket)
+ *
+ * Converts `query`, a Watchman query comprising Ruby objects, into the Watchman
+ * binary protocol format, transmits it over socket, and unserializes and
+ * returns the result.
+ */
+VALUE CommandTWatchmanUtils_query(VALUE self, VALUE query, VALUE socket) {
+    char *payload;
+    int fileno, flags;
+    int8_t peek[WATCHMAN_PEEK_BUFFER_SIZE];
+    int8_t sizes[] = { 0, 0, 0, 1, 2, 4, 8 };
+    int8_t sizes_idx;
+    int8_t *pdu_size_ptr;
+    int64_t payload_size;
+    long query_len;
+    ssize_t peek_size, sent, received;
+    void *buffer;
+    VALUE loaded, serialized;
+    fileno = NUM2INT(rb_funcall(socket, rb_intern("fileno"), 0));
+
+    // do blocking I/O to simplify the following logic
+    flags = fcntl(fileno, F_GETFL);
+    if (fcntl(fileno, F_SETFL, flags & ~O_NONBLOCK) == -1) {
+        rb_raise(rb_eRuntimeError, "unable to clear O_NONBLOCK flag");
+    }
+
+    // send the message
+    serialized = CommandTWatchmanUtils_dump(self, query);
+    query_len = RSTRING_LEN(serialized);
+    sent = send(fileno, RSTRING_PTR(serialized), query_len, 0);
+    if (sent == -1) {
+        watchman_raise_system_call_error(errno);
+    } else if (sent != query_len) {
+        rb_raise(rb_eRuntimeError, "expected to send %ld bytes but sent %zd",
+            query_len, sent);
+    }
+
+    // sniff to see how large the header is
+    received = recv(fileno, peek, WATCHMAN_SNIFF_BUFFER_SIZE, MSG_PEEK | MSG_WAITALL);
+    if (received == -1) {
+        watchman_raise_system_call_error(errno);
+    } else if (received != WATCHMAN_SNIFF_BUFFER_SIZE) {
+        rb_raise(rb_eRuntimeError, "failed to sniff PDU header");
+    }
+
+    // peek at size of PDU
+    sizes_idx = peek[sizeof(WATCHMAN_BINARY_MARKER) - 1];
+    if (sizes_idx < WATCHMAN_INT8_MARKER || sizes_idx > WATCHMAN_INT64_MARKER) {
+        rb_raise(rb_eRuntimeError, "bad PDU size marker");
+    }
+    peek_size = sizeof(WATCHMAN_BINARY_MARKER) - 1 + sizeof(int8_t) +
+        sizes[sizes_idx];
+
+    received = recv(fileno, peek, peek_size, MSG_PEEK);
+    if (received == -1) {
+        watchman_raise_system_call_error(errno);
+    } else if (received != peek_size) {
+        rb_raise(rb_eRuntimeError, "failed to peek at PDU header");
+    }
+    pdu_size_ptr = peek + sizeof(WATCHMAN_BINARY_MARKER) - sizeof(int8_t);
+    payload_size =
+        peek_size +
+        watchman_load_int((char **)&pdu_size_ptr, (char *)peek + peek_size);
+
+    // actually read the PDU
+    buffer = xmalloc(payload_size);
+    if (!buffer) {
+        rb_raise(
+            rb_eNoMemError,
+            "failed to allocate %lld bytes",
+            (long long int)payload_size
+        );
+    }
+    received = recv(fileno, buffer, payload_size, MSG_WAITALL);
+    if (received == -1) {
+        watchman_raise_system_call_error(errno);
+    } else if (received != payload_size) {
+        rb_raise(rb_eRuntimeError, "failed to load PDU");
+    }
+    payload = (char *)buffer + peek_size;
+    loaded = watchman_load(&payload, payload + payload_size);
+    free(buffer);
+    return loaded;
+}
+
+#else /* don't build Watchman utils; supply stubs only*/
+
+VALUE CommandTWatchmanUtils_load(VALUE self, VALUE serialized) {
+    rb_raise(rb_eRuntimeError, "unsupported operation");
+}
+
+VALUE CommandTWatchmanUtils_dump(VALUE self, VALUE serializable) {
+    rb_raise(rb_eRuntimeError, "unsupported operation");
+}
+
+VALUE CommandTWatchmanUtils_query(VALUE self, VALUE query, VALUE socket) {
+    rb_raise(rb_eRuntimeError, "unsupported operation");
+}
+
+#endif
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/watchman.h b/tests/fixtures/integration/command-t/input/ruby/command-t/ext/command-t/watchman.h
new file mode 100644 (file)
index 0000000..b10f151
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright 2014-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the BSD 2-clause license.
+
+#include <ruby.h>
+
+/**
+ * @module CommandT::Watchman::Utils
+ *
+ * Methods for working with the Watchman binary protocol
+ *
+ * @see https://github.com/facebook/watchman/blob/master/website/_docs/BSER.markdown
+ */
+
+/**
+ * Convert an object serialized using the Watchman binary protocol[0] into an
+ * unpacked Ruby object
+ */
+extern VALUE CommandTWatchmanUtils_load(VALUE self, VALUE serialized);
+
+/**
+ * Serialize a Ruby object into the Watchman binary protocol format
+ */
+extern VALUE CommandTWatchmanUtils_dump(VALUE self, VALUE serializable);
+
+/**
+ * Issue `query` to the Watchman instance listening on `socket` (a `UNIXSocket`
+ * instance) and return the result
+ *
+ * The query is serialized following the Watchman binary protocol and the
+ * result is converted to native Ruby objects before returning to the caller.
+ */
+extern VALUE CommandTWatchmanUtils_query(VALUE self, VALUE query, VALUE socket);
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t.rb
new file mode 100644 (file)
index 0000000..ed4e6b1
--- /dev/null
@@ -0,0 +1,24 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  begin
+    require 'command-t/metadata'
+  rescue LoadError
+    require 'command-t/metadata/fallback'
+  end
+
+  autoload :Controller, 'command-t/controller'
+  autoload :Finder, 'command-t/finder'
+  autoload :MRU, 'command-t/mru'
+  autoload :MatchWindow, 'command-t/match_window'
+  autoload :PathUtilities, 'command-t/path_utilities'
+  autoload :ProgressReporter, 'command-t/progress_reporter'
+  autoload :Prompt, 'command-t/prompt'
+  autoload :SCMUtilities, 'command-t/scm_utilities'
+  autoload :Scanner, 'command-t/scanner'
+  autoload :Settings, 'command-t/settings'
+  autoload :Stub, 'command-t/stub'
+  autoload :Util, 'command-t/util'
+  autoload :VIM, 'command-t/vim'
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/controller.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/controller.rb
new file mode 100644 (file)
index 0000000..9d665c8
--- /dev/null
@@ -0,0 +1,594 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Controller
+    include PathUtilities
+    include SCMUtilities
+
+    # Wraps `method` in a `rescue` clause that attempts to print some useful
+    # information to the screen before re-raising any exception. Without this,
+    # most of the useful output is unhelpfully swallowed by Vim.
+    def self.guard(method)
+      class_eval <<-END
+        alias original_#{method} #{method}
+        def #{method}(*args, &block)
+          original_#{method}(*args, &block)
+        rescue Exception => e
+          backtrace = e.backtrace
+          trimmed = backtrace.take(backtrace.length - 2)
+          text = VIM::escape_for_single_quotes trimmed.join("\n")
+          ::VIM::command "echo '\#{text}'"
+          raise e
+        end
+      END
+    end
+
+    def initialize
+      encoding = VIM::get_string('g:CommandTEncoding')
+      if encoding
+        begin
+          encoding = Encoding.find(encoding)
+          Encoding.default_external = encoding
+          Encoding.default_internal = encoding
+        rescue
+        end
+      end
+      @nowait = '<nowait>' if ::VIM::evaluate('v:version') >= 704
+    end
+
+    # For possible use in status lines.
+    def active_finder
+      @active_finder && @active_finder.class.name
+    end
+
+    # For possible use in status lines.
+    def path
+      @path
+    end
+
+    # For possible use in status lines.
+    def is_own_buffer(buffer_number)
+      @match_window && buffer_number == @match_window.buffer_number
+    end
+
+    # For possible use in status lines.
+    def return_is_own_buffer(buffer_number)
+      if is_own_buffer(buffer_number)
+        ::VIM::command 'return 1'
+      else
+        ::VIM::command 'return 0'
+      end
+    end
+
+    def show_buffer_finder
+      @path          = VIM::pwd
+      @active_finder = buffer_finder
+      show
+    end
+    guard :show_buffer_finder
+
+    def show_command_finder
+      @path          = VIM::pwd
+      @active_finder = command_finder
+      show
+    end
+    guard :show_command_finder
+
+    def show_help_finder
+      @path          = VIM::pwd
+      @active_finder = help_finder
+      show
+    end
+    guard :show_help_finder
+
+    def show_history_finder
+      @path          = VIM::pwd
+      @active_finder = history_finder
+      show
+    end
+    guard :show_history_finder
+
+    def show_jump_finder
+      @path          = VIM::pwd
+      @active_finder = jump_finder
+      show
+    end
+    guard :show_jump_finder
+
+    def show_line_finder
+      @path          = VIM::pwd
+      @active_finder = line_finder
+      show
+    end
+    guard :show_line_finder
+
+    def show_mru_finder
+      @path          = VIM::pwd
+      @active_finder = mru_finder
+      show
+    end
+    guard :show_mru_finder
+
+    def show_search_finder
+      @path          = VIM::pwd
+      @active_finder = search_finder
+      show
+    end
+    guard :show_search_finder
+
+    def show_tag_finder
+      @path          = VIM::pwd
+      @active_finder = tag_finder
+      show
+    end
+    guard :show_tag_finder
+
+    def show_file_finder
+      # optional parameter will be desired starting directory, or ""
+
+      arg = ::VIM::evaluate('a:arg')
+      if arg && arg.size > 0
+        @path = File.expand_path(arg, VIM::pwd)
+      else
+        traverse = VIM::get_string('g:CommandTTraverseSCM') || 'file'
+        case traverse
+        when 'file'
+          @path = nearest_ancestor(VIM::current_file_dir, scm_markers) || VIM::pwd
+        when 'dir'
+          @path = nearest_ancestor(VIM::pwd, scm_markers) || VIM::pwd
+        else
+          @path = VIM::pwd
+        end
+      end
+
+      @active_finder    = file_finder
+      file_finder.path  = @path
+      show
+    rescue Errno::ENOENT
+      # probably a problem with the optional parameter
+      @match_window.print_no_such_file_or_directory
+    end
+    guard :show_file_finder
+
+    def hide
+      @match_window.leave
+      if VIM::Window.select @initial_window
+        if @initial_buffer.number == 0
+          # upstream bug: buffer number misreported as 0
+          # see: https://wincent.com/issues/1617
+          ::VIM::command "silent b #{@initial_buffer.name}"
+        else
+          ::VIM::command "silent b #{@initial_buffer.number}"
+        end
+      end
+    end
+
+    # Take current matches and stick them in the quickfix window.
+    def quickfix
+      hide
+
+      matches = @matches.map do |match|
+        "{ 'filename': '#{VIM::escape_for_single_quotes match}' }"
+      end.join(', ')
+
+      ::VIM::command 'call setqflist([' + matches + '])'
+      ::VIM::command 'cope'
+    end
+    guard :quickfix
+
+    def refresh
+      return unless @active_finder && @active_finder.respond_to?(:flush)
+      @active_finder.flush
+      list_matches!
+    end
+    guard :refresh
+
+    def flush
+      @file_finder  = nil
+      @max_height   = nil
+      @min_height   = nil
+      @prompt       = nil
+      @tag_finder   = nil
+    end
+    guard :flush
+
+    def handle_key
+      key = ::VIM::evaluate('a:arg').to_i.chr
+      if @focus == prompt
+        prompt.add! key
+        update
+      else
+        @match_window.find key
+      end
+    end
+    guard :handle_key
+
+    def backspace
+      if @focus == prompt
+        prompt.backspace!
+        update
+      end
+    end
+    guard :backspace
+
+    def delete
+      if @focus == prompt
+        prompt.delete!
+        update
+      end
+    end
+    guard :delete
+
+    def accept_selection(options = {})
+      selection = @match_window.selection
+      hide
+      open_selection(selection, options) unless selection.nil?
+    end
+    guard :accept_selection
+
+    def toggle_focus
+      @focus.unfocus # old focus
+      @focus = @focus == prompt ? @match_window : prompt
+      @focus.focus # new focus
+    end
+    guard :toggle_focus
+
+    def cancel
+      hide
+    end
+    guard :cancel
+
+    def select_next
+      @match_window.select_next
+    end
+    guard :select_next
+
+    def select_prev
+      @match_window.select_prev
+    end
+    guard :select_prev
+
+    def clear
+      prompt.clear!
+      list_matches!
+    end
+    guard :clear
+
+    def clear_prev_word
+      prompt.clear_prev_word!
+      list_matches!
+    end
+    guard :clear_prev_word
+
+    def cursor_left
+      prompt.cursor_left if @focus == prompt
+    end
+    guard :cursor_left
+
+    def cursor_right
+      prompt.cursor_right if @focus == prompt
+    end
+    guard :cursor_right
+
+    def cursor_end
+      prompt.cursor_end if @focus == prompt
+    end
+    guard :cursor_end
+
+    def cursor_start
+      prompt.cursor_start if @focus == prompt
+    end
+    guard :cursor_start
+
+    def leave
+      @match_window.leave
+    end
+
+    def unload
+      @match_window.unload
+    end
+
+    def list_matches(options = {})
+      return unless @needs_update || options[:force]
+
+      @matches = @active_finder.sorted_matches_for(
+        prompt.abbrev,
+        :case_sensitive => case_sensitive?,
+        :limit          => match_limit,
+        :threads        => CommandT::Util.processor_count,
+        :ignore_spaces  => VIM::get_bool('g:CommandTIgnoreSpaces', true),
+        :recurse        => VIM::get_bool('g:CommandTRecursiveMatch', true)
+      )
+      @match_window.matches = @matches
+
+      # Scanner may have overwritten prompt to show progress.
+      prompt.redraw
+
+      @needs_update = false
+    end
+    guard :list_matches
+
+    def tab_command
+      VIM::get_string('g:CommandTAcceptSelectionTabCommand') || 'tabe'
+    end
+
+    def split_command
+      VIM::get_string('g:CommandTAcceptSelectionSplitCommand') || 'sp'
+    end
+
+    def vsplit_command
+      VIM::get_string('g:CommandTAcceptSelectionVSplitCommand') || 'vs'
+    end
+
+  private
+
+    def update
+      if @debounce_interval > 0
+        @needs_update = true
+      else
+        list_matches!
+      end
+    end
+
+    def prompt
+      @prompt ||= Prompt.new(
+        :cursor_color => VIM::get_string('g:CommandTCursorColor')
+      )
+    end
+
+    def scm_markers
+      markers = VIM::get_string('g:CommandTSCMDirectories')
+      markers = markers && markers.split(/\s*,\s*/)
+      markers = %w[.git .hg .svn .bzr _darcs] unless markers && markers.any?
+      markers
+    end
+
+    def list_matches!
+      list_matches(:force => true)
+    end
+
+    def show
+      @initial_window = $curwin
+      @initial_buffer = $curbuf
+      @debounce_interval = VIM::get_number('g:CommandTInputDebounce') || 0
+      @match_window = MatchWindow.new \
+        :encoding             => VIM::get_string('g:CommandTEncoding'),
+        :highlight_color      => VIM::get_string('g:CommandTHighlightColor'),
+        :match_window_at_top  => VIM::get_bool('g:CommandTMatchWindowAtTop'),
+        :match_window_reverse => VIM::get_bool('g:CommandTMatchWindowReverse', true),
+        :min_height           => min_height,
+        :debounce_interval    => @debounce_interval,
+        :prompt               => prompt,
+        :name                 => "Command-T [#{@active_finder.name}]"
+      @focus            = prompt
+      prompt.focus
+      register_for_key_presses
+      set_up_autocmds
+      clear # clears prompt and lists matches
+    end
+
+    def max_height
+      @max_height ||= VIM::get_number('g:CommandTMaxHeight') || 15
+    end
+
+    def min_height
+      @min_height ||= begin
+        min_height = VIM::get_number('g:CommandTMinHeight') || 0
+        min_height = max_height if max_height != 0 && min_height > max_height
+        min_height
+      end
+    end
+
+    def case_sensitive?
+      if prompt.abbrev.match(/[A-Z]/)
+        if VIM::exists?('g:CommandTSmartCase')
+          smart_case = VIM::get_bool('g:CommandTSmartCase')
+        else
+          smart_case = VIM::get_bool('&smartcase')
+        end
+
+        if smart_case
+          return true
+        end
+      end
+
+      if VIM::exists?('g:CommandTIgnoreCase')
+        return !VIM::get_bool('g:CommandTIgnoreCase')
+      end
+
+      false
+    end
+
+    # Backslash-escape space, \, |, %, #, "
+    def sanitize_path_string(str)
+      # for details on escaping command-line mode arguments see: :h :
+      # (that is, help on ":") in the Vim documentation.
+      str.gsub(/[ \\|%#"]/, '\\\\\0')
+    end
+
+    def current_buffer_visible_in_other_window
+      count = (0...::VIM::Window.count).to_a.inject(0) do |acc, i|
+        acc += 1 if ::VIM::Window[i].buffer.number == $curbuf.number
+        acc
+      end
+      count > 1
+    end
+
+    def default_open_command
+      if !VIM::get_bool('&modified') ||
+        VIM::get_bool('&hidden') ||
+        VIM::get_bool('&autowriteall') && !VIM::get_bool('&readonly') ||
+        current_buffer_visible_in_other_window
+        VIM::get_string('g:CommandTAcceptSelectionCommand') || 'e'
+      else
+        'sp'
+      end
+    end
+
+    def ensure_appropriate_window_selection
+      # normally we try to open the selection in the current window, but there
+      # is one exception:
+      #
+      # - we don't touch any "unlisted" buffer with buftype "nofile" (such as
+      #   NERDTree or MiniBufExplorer); this is to avoid things like the "Not
+      #   enough room" error which occurs when trying to open in a split in a
+      #   shallow (potentially 1-line) buffer like MiniBufExplorer is current
+      #
+      # Other "unlisted" buffers, such as those with buftype "help" are treated
+      # normally.
+      initial = $curwin
+      while true do
+        break unless ::VIM::evaluate('&buflisted').to_i == 0 &&
+          ::VIM::evaluate('&buftype').to_s == 'nofile'
+        ::VIM::command 'wincmd w'     # try next window
+        break if $curwin == initial # have already tried all
+      end
+    end
+
+    def open_selection(selection, options = {})
+      command = options[:command] || default_open_command
+      selection = File.expand_path selection, @path
+      selection = relative_path_under_working_directory selection
+      selection = sanitize_path_string selection
+      selection = File.join('.', selection) if selection =~ /^\+/
+      ensure_appropriate_window_selection
+
+      @active_finder.open_selection command, selection, options
+    end
+
+    def map(key, function, param = nil)
+      ::VIM::command "noremap <silent> <buffer> #{@nowait} #{key} " \
+        ":call commandt#private##{function}(#{param})<CR>"
+    end
+
+    def term
+      @term ||= ::VIM::evaluate('&term')
+    end
+
+    def register_for_key_presses
+      # "normal" keys (interpreted literally)
+      numbers     = ('0'..'9').to_a.join
+      lowercase   = ('a'..'z').to_a.join
+      uppercase   = lowercase.upcase
+      punctuation = '<>`@#~!"$%^&/()=+*-_.,;:?\\|\'{}[]'
+      space       = ' '
+      (numbers + lowercase + uppercase + punctuation + space).each_byte do |b|
+        map "<Char-#{b}>", 'HandleKey', b
+      end
+
+      # "special" keys (overridable by settings)
+      {
+        'AcceptSelection'       => '<CR>',
+        'AcceptSelectionSplit'  => ['<C-CR>', '<C-s>'],
+        'AcceptSelectionTab'    => '<C-t>',
+        'AcceptSelectionVSplit' => '<C-v>',
+        'Backspace'             => '<BS>',
+        'Cancel'                => ['<C-c>', '<Esc>'],
+        'Clear'                 => '<C-u>',
+        'ClearPrevWord'         => '<C-w>',
+        'CursorEnd'             => '<C-e>',
+        'CursorLeft'            => ['<Left>', '<C-h>'],
+        'CursorRight'           => ['<Right>', '<C-l>'],
+        'CursorStart'           => '<C-a>',
+        'Delete'                => '<Del>',
+        'Quickfix'              => '<C-q>',
+        'Refresh'               => '<C-f>',
+        'SelectNext'            => ['<C-n>', '<C-j>', '<Down>'],
+        'SelectPrev'            => ['<C-p>', '<C-k>', '<Up>'],
+        'ToggleFocus'           => '<Tab>',
+      }.each do |key, value|
+        if override = VIM::get_list_or_string("g:CommandT#{key}Map")
+          Array(override).each do |mapping|
+            map mapping, key
+          end
+        else
+          Array(value).each do |mapping|
+            unless mapping == '<Esc>' && term =~ /\A(rxvt|screen|vt100|xterm)/
+              map mapping, key
+            end
+          end
+        end
+      end
+    end
+
+    def set_up_autocmds
+      if @debounce_interval > 0
+        ::VIM::command 'augroup CommandTController'
+        ::VIM::command 'autocmd!'
+        ::VIM::command 'autocmd CursorHold <buffer> :call commandt#private#ListMatches()'
+        ::VIM::command 'augroup END'
+      end
+    end
+
+    # Returns the desired maximum number of matches, based on available vertical
+    # space and the g:CommandTMaxHeight option.
+    #
+    # Note the "available" space is actually a theoretical upper bound; it takes
+    # into account screen dimensions but not things like existing splits which
+    # may reduce the amount of space in practice.
+    def match_limit
+      limit = [1, VIM::Screen.lines - 5].max
+      limit = [limit, max_height].min if max_height > 0
+      limit
+    end
+
+    def buffer_finder
+      @buffer_finder ||= CommandT::Finder::BufferFinder.new
+    end
+
+    def command_finder
+      @command_finder ||= CommandT::Finder::CommandFinder.new
+    end
+
+    def mru_finder
+      @mru_finder ||= CommandT::Finder::MRUBufferFinder.new
+    end
+
+    def wildignore
+      ignore = VIM::get_string('g:CommandTWildIgnore')
+      if ignore.nil? && VIM::exists?('&wildignore')
+        ignore = ::VIM::evaluate('&wildignore').to_s
+      end
+      VIM::wildignore_to_regexp(ignore) unless ignore.nil?
+    end
+
+    def file_finder
+      @file_finder ||= CommandT::Finder::FileFinder.new nil,
+        :max_depth              => VIM::get_number('g:CommandTMaxDepth'),
+        :max_files              => VIM::get_number('g:CommandTMaxFiles'),
+        :max_caches             => VIM::get_number('g:CommandTMaxCachedDirectories'),
+        :always_show_dot_files  => VIM::get_bool('g:CommandTAlwaysShowDotFiles'),
+        :never_show_dot_files   => VIM::get_bool('g:CommandTNeverShowDotFiles'),
+        :scan_dot_directories   => VIM::get_bool('g:CommandTScanDotDirectories'),
+        :wildignore             => wildignore,
+        :scanner                => VIM::get_string('g:CommandTFileScanner'),
+        :git_scan_submodules    => VIM::get_bool('g:CommandTGitScanSubmodules')
+    end
+
+    def help_finder
+      @jump_finder ||= CommandT::Finder::HelpFinder.new
+    end
+
+    def history_finder
+      CommandT::Finder::HistoryFinder.new(:history_type => ':')
+    end
+
+    def jump_finder
+      @jump_finder ||= CommandT::Finder::JumpFinder.new
+    end
+
+    def line_finder
+      CommandT::Finder::LineFinder.new
+    end
+
+    def search_finder
+      CommandT::Finder::HistoryFinder.new(:history_type => '/')
+    end
+
+    def tag_finder
+      @tag_finder ||= CommandT::Finder::TagFinder.new \
+        :include_filenames => VIM::get_bool('g:CommandTTagIncludeFilenames')
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder.rb
new file mode 100644 (file)
index 0000000..6fc62bc
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'command-t/ext' # CommandT::Matcher, CommandT::Watchman::Utils
+
+module CommandT
+  # Encapsulates a Scanner instance (which builds up a list of available files
+  # in a directory) and a Matcher instance (which selects from that list based
+  # on a search string).
+  #
+  # Specialized subclasses use different kinds of scanners adapted for
+  # different kinds of search (files, buffers).
+  class Finder
+    autoload :BufferFinder,    'command-t/finder/buffer_finder'
+    autoload :CommandFinder,   'command-t/finder/command_finder'
+    autoload :FileFinder,      'command-t/finder/file_finder'
+    autoload :HelpFinder,      'command-t/finder/help_finder'
+    autoload :HistoryFinder,   'command-t/finder/history_finder'
+    autoload :JumpFinder,      'command-t/finder/jump_finder'
+    autoload :LineFinder,      'command-t/finder/line_finder'
+    autoload :MRUBufferFinder, 'command-t/finder/mru_buffer_finder'
+    autoload :TagFinder,       'command-t/finder/tag_finder'
+
+    include PathUtilities
+
+    def initialize(path = Dir.pwd, options = {})
+      raise RuntimeError, 'Subclass responsibility'
+    end
+
+    # Returns a human-readable name describing the finder, for display in the
+    # statusline attached to the MatchWindow buffer.
+    def name
+      raise RuntimeError, 'Subclass responsibility'
+    end
+
+    # Options:
+    #   :limit (integer): limit the number of returned matches
+    def sorted_matches_for(str, options = {})
+      @matcher.sorted_matches_for str, options
+    end
+
+    def open_selection(command, selection, options = {})
+      ::VIM::command "silent #{command} #{selection}"
+    end
+
+    def path=(path)
+      @scanner.path = path
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/buffer_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/buffer_finder.rb
new file mode 100644 (file)
index 0000000..1f04571
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class BufferFinder < Finder
+      def initialize
+        @scanner = Scanner::BufferScanner.new
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def name
+        'Buffers'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/command_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/command_finder.rb
new file mode 100644 (file)
index 0000000..0212a97
--- /dev/null
@@ -0,0 +1,23 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class CommandFinder < Finder
+      def initialize(options = {})
+        @scanner = Scanner::CommandScanner.new
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def open_selection(command, selection, options = {})
+        ::VIM::command "call feedkeys(':#{selection} ', 'nt')"
+      end
+
+      def flush; end
+
+      def name
+        'Commands'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/file_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/file_finder.rb
new file mode 100644 (file)
index 0000000..9daa9d5
--- /dev/null
@@ -0,0 +1,33 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class FileFinder < Finder
+      def initialize(path = Dir.pwd, options = {})
+        case options.delete(:scanner)
+        when 'ruby', nil # ruby is the default
+          @scanner = Scanner::FileScanner::RubyFileScanner.new(path, options)
+        when 'find'
+          @scanner = Scanner::FileScanner::FindFileScanner.new(path, options)
+        when 'watchman'
+          @scanner = Scanner::FileScanner::WatchmanFileScanner.new(path, options)
+        when 'git'
+          @scanner = Scanner::FileScanner::GitFileScanner.new(path, options)
+        else
+          raise ArgumentError, "unknown scanner type '#{options[:scanner]}'"
+        end
+
+        @matcher = Matcher.new @scanner, options
+      end
+
+      def flush
+        @scanner.flush
+      end
+
+      def name
+        'Files'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/help_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/help_finder.rb
new file mode 100644 (file)
index 0000000..10e149c
--- /dev/null
@@ -0,0 +1,25 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class HelpFinder < Finder
+      def initialize(options = {})
+        @scanner = Scanner::HelpScanner.new
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def open_selection(command, selection, options = {})
+        ::VIM::command "help #{selection}"
+      end
+
+      def flush
+        @scanner.flush
+      end
+
+      def name
+        'Help'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/history_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/history_finder.rb
new file mode 100644 (file)
index 0000000..7622c85
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class HistoryFinder < Finder
+      def initialize(options = {})
+        @history_type = options[:history_type] # / or :
+        @scanner = Scanner::HistoryScanner.new("silent history #{@history_type}")
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def open_selection(command, selection, options = {})
+        # Need to unescape to reverse the work done by `#sanitize_path_string`.
+        unescaped = selection.gsub(/\\(.)/, '\1')
+        escaped = VIM.escape_for_single_quotes unescaped
+        ::VIM::command "call feedkeys('#{@history_type}#{escaped} ', 'nt')"
+      end
+
+      def flush; end
+
+      def name
+        @history_type == ':' ? 'History' : 'Searches'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/jump_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/jump_finder.rb
new file mode 100644 (file)
index 0000000..2680713
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class JumpFinder < Finder
+      def initialize
+        @scanner = Scanner::JumpScanner.new
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def name
+        'Jumps'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/line_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/line_finder.rb
new file mode 100644 (file)
index 0000000..1a076f8
--- /dev/null
@@ -0,0 +1,23 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class LineFinder < Finder
+      def initialize(options = {})
+        @scanner = Scanner::LineScanner.new
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def open_selection(command, selection, options = {})
+        ::VIM::command "#{selection.sub(/.+:(\d+)$/, '\1')}"
+      end
+
+      def flush; end
+
+      def name
+        'Lines'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/mru_buffer_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/mru_buffer_finder.rb
new file mode 100644 (file)
index 0000000..c7d4ef2
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class MRUBufferFinder < BufferFinder
+      def initialize
+        @scanner = Scanner::MRUBufferScanner.new
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      # Override sorted_matches_for to prevent MRU ordered matches from being
+      # ordered alphabetically.
+      def sorted_matches_for(str, options = {})
+        matches = super(str, options.merge(:sort => false))
+
+        # take current buffer (by definition, the most recently used) and move it
+        # to the end of the results
+        if MRU.last &&
+          relative_path_under_working_directory(MRU.last.name) == matches.first
+          matches[1..-1] + [matches.first]
+        else
+          matches
+        end
+      end
+
+      def name
+        'Recent'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/tag_finder.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/finder/tag_finder.rb
new file mode 100644 (file)
index 0000000..b3be93a
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Finder
+    class TagFinder < Finder
+      def initialize(options = {})
+        @scanner = Scanner::TagScanner.new options
+        @matcher = Matcher.new @scanner, :always_show_dot_files => true
+      end
+
+      def open_selection(command, selection, options = {})
+        if @scanner.include_filenames
+          selection = selection[0, selection.index(':')]
+        end
+
+        #  open the tag and center the screen on it
+        ::VIM::command "silent! tag #{selection} | :normal zz"
+      end
+
+      def flush
+        @scanner.flush
+      end
+
+      def name
+        'Tags'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/match_window.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/match_window.rb
new file mode 100644 (file)
index 0000000..87b39e9
--- /dev/null
@@ -0,0 +1,537 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'ostruct'
+
+module CommandT
+  class MatchWindow
+    SELECTION_MARKER  = '> '
+    MARKER_LENGTH     = SELECTION_MARKER.length
+    UNSELECTED_MARKER = ' ' * MARKER_LENGTH
+    MH_START          = '<commandt>'
+    MH_END            = '</commandt>'
+    @@buffer          = nil
+
+    Highlight = Struct.new(:highlight, :bang)
+
+    def initialize(options = {})
+      @encoding        = options[:encoding]
+      @highlight_color = options[:highlight_color] || 'PmenuSel'
+      @min_height      = options[:min_height]
+      @prompt          = options[:prompt]
+      @reverse_list    = options[:match_window_reverse]
+
+      quoted_name = VIM::escape_for_single_quotes(options[:name])
+      escaped_name = ::VIM::evaluate("fnameescape('#{quoted_name}')")
+
+      run_will_show_autocmd
+
+      set 'timeout', true        # ensure mappings timeout
+      set 'hlsearch', false      # don't highlight search strings
+      set 'insertmode', false    # don't make Insert mode the default
+      set 'showcmd', false       # don't show command info on last line
+      set 'equalalways', false   # don't auto-balance window sizes
+      set 'timeoutlen', 0        # respond immediately to mappings
+      set 'report', 9999         # don't show "X lines changed" reports
+      set 'scrolloff', 0         # don't scroll near buffer edges
+      set 'sidescroll', 0        # don't sidescroll in jumps
+      set 'sidescrolloff', 0     # don't sidescroll automatically
+
+      if options[:debounce_interval] > 0
+        set 'updatetime', options[:debounce_interval]
+      end
+
+      # Save existing window views so we can restore them later.
+      current_window = ::VIM::evaluate('winnr()')
+      @windows = (0..(::VIM::Window.count - 1)).map do |i|
+        focus_window(i + 1)
+        view = ::VIM::evaluate('winsaveview()')
+        window = OpenStruct.new(
+          :index    => i,
+          :height   => ::VIM::Window[i].height,
+          :width    => ::VIM::Window[i].width,
+          :lnum     => view['lnum'],
+          :col      => view['col'],
+          :coladd   => view['coladd'],
+          :curswant => view['curswant'],
+          :topline  => view['topline'],
+          :topfill  => view['topfill'],
+          :leftcol  => view['leftcol'],
+          :skipcol  => view['skipcol']
+        )
+
+        # When creating a split for the match window, move the cursor to the
+        # opposite side of the window's viewport to prevent unwanted scrolling.
+        boundary_line = options[:match_window_at_top] ?
+          ::VIM::evaluate("line('w$')") :
+          view['topline']
+        ::VIM::evaluate("winrestview({'lnum': #{boundary_line}})")
+
+        window
+      end
+      focus_window(current_window)
+
+      # show match window
+      split_location = options[:match_window_at_top] ? 'topleft' : 'botright'
+      if ((number = buffer_number)) # still have buffer from last time
+        ::VIM::command "silent! #{split_location} #{number}sbuffer"
+        if $curbuf.number != number
+          raise "Can't re-open Command-T match listing buffer"
+        end
+        $curwin.height = 1
+        ::VIM::command "0file"
+        ::VIM::command "keepalt file #{escaped_name}"
+      else        # creating match window for first time and set it up
+        ::VIM::command "silent! keepalt #{split_location} 1split #{escaped_name}"
+        set 'bufhidden', 'unload'   # unload buf when no longer displayed
+        set 'buftype', 'nofile'     # buffer is not related to any file
+        set 'filetype', 'command-t' # provide for detectability/extensibility
+        set 'modifiable', false     # prevent manual edits
+        set 'readonly', false       # avoid W10 "Changing a readonly file"
+        set 'swapfile', false       # don't create a swapfile
+        set 'wrap', false           # don't soft-wrap
+        set 'number', false         # don't show line numbers
+        set 'list', false           # don't use List mode (visible tabs etc)
+        set 'foldcolumn', 0         # don't show a fold column at side
+        set 'foldlevel', 99         # don't fold anything
+        set 'cursorline', false     # don't highlight line cursor is on
+        set 'spell', false          # spell-checking off
+        set 'buflisted', false      # don't show up in the buffer list
+        set 'textwidth', 0          # don't hard-wrap (break long lines)
+
+        # don't show the color column
+        set 'colorcolumn', 0 if VIM::exists?('+colorcolumn')
+
+        # don't show relative line numbers
+        set 'relativenumber', false if VIM::exists?('+relativenumber')
+
+        # sanity check: make sure the buffer really was created
+        if File.basename($curbuf.name) != options[:name]
+          raise "Can't find Command-T match listing buffer"
+        end
+        @@buffer = $curbuf
+      end
+
+      # syntax coloring
+      if VIM::has?('syntax')
+        ::VIM::command "syntax match CommandTSelection \"^#{SELECTION_MARKER}.\\+$\""
+        ::VIM::command 'syntax match CommandTNoEntries "^-- NO MATCHES --$"'
+        ::VIM::command 'syntax match CommandTNoEntries "^-- NO SUCH FILE OR DIRECTORY --$"'
+        set 'synmaxcol', 9999
+
+        if VIM::has?('conceal')
+          set 'conceallevel', 2
+          set 'concealcursor', 'nvic'
+          ::VIM::command 'syntax region CommandTCharMatched ' \
+                         "matchgroup=CommandTCharMatched start=+#{MH_START}+ " \
+                         "matchgroup=CommandTCharMatchedEnd end=+#{MH_END}+ concealends"
+          ::VIM::command 'highlight def CommandTCharMatched ' \
+                         'term=bold,underline cterm=bold,underline ' \
+                         'gui=bold,underline'
+        end
+
+        ::VIM::command "highlight link CommandTSelection #{@highlight_color}"
+        ::VIM::command 'highlight link CommandTNoEntries Error'
+
+        # hide cursor
+        @cursor_highlight = get_cursor_highlight
+        hide_cursor
+      end
+
+      # perform cleanup using an autocmd to ensure we don't get caught out
+      # by some unexpected means of dismissing or leaving the Command-T window
+      # (eg. <C-W q>, <C-W k> etc)
+      ::VIM::command 'augroup CommandTMatchWindow'
+      ::VIM::command 'autocmd!'
+      ::VIM::command 'autocmd BufLeave <buffer> silent! ruby $command_t.leave'
+      ::VIM::command 'autocmd BufUnload <buffer> silent! ruby $command_t.unload'
+      ::VIM::command 'augroup END'
+
+      @has_focus  = false
+      @abbrev     = ''
+      @window     = $curwin
+    end
+
+    def buffer_number
+      @@buffer && @@buffer.number
+    rescue Vim::DeletedBufferError
+      # Beware of people manually deleting Command-T's hidden, unlisted buffer.
+      @@buffer = nil
+    end
+
+    def close
+      # Unlisted buffers like those provided by Netrw, NERDTree and Vim's help
+      # don't actually appear in the buffer list; if they are the only such
+      # buffers present when Command-T is invoked (for example, when invoked
+      # immediately after starting Vim with a directory argument, like `vim .`)
+      # then performing the normal clean-up will yield an "E90: Cannot unload
+      # last buffer" error. We can work around that by doing a :quit first.
+      if ::VIM::Buffer.count == 0
+        ::VIM::command 'silent quit'
+      end
+
+      # Workaround for upstream bug in Vim 7.3 on some platforms
+      #
+      # On some platforms, $curbuf.number always returns 0. One workaround is
+      # to build Vim with --disable-largefile, but as this is producing lots of
+      # support requests, implement the following fallback to the buffer name
+      # instead, at least until upstream gets fixed.
+      #
+      # For more details, see: https://wincent.com/issues/1617
+      if $curbuf.number == 0
+        # use bwipeout as bunload fails if passed the name of a hidden buffer
+        base = File.basename($curbuf.name)
+        escaped_name = ::VIM::evaluate("fnameescape('#{base}')")
+        ::VIM::command "silent! bwipeout! #{escaped_name}"
+        @@buffer = nil
+      else
+        ::VIM::command "silent! bunload! #{@@buffer.number}"
+      end
+    end
+
+    def leave
+      close
+      unload
+    end
+
+    def unload
+      restore_window_views
+      @prompt.dispose
+      @settings.restore
+      show_cursor
+      run_did_hide_autocmd
+    end
+
+    def add!(char)
+      @abbrev += char
+    end
+
+    def backspace!
+      @abbrev.chop!
+    end
+
+    def select_next
+      @reverse_list ? _prev : _next
+    end
+
+    def select_prev
+      @reverse_list ? _next : _prev
+    end
+
+    def matches=(matches)
+      if matches != @matches
+        @matches = matches
+        @selection = 0
+        print_matches
+        move_cursor_to_selected_line
+      end
+    end
+
+    def focus
+      unless @has_focus
+        @has_focus = true
+        if VIM::has?('syntax')
+          ::VIM::command 'highlight link CommandTSelection Search'
+        end
+      end
+    end
+
+    def unfocus
+      if @has_focus
+        @has_focus = false
+        if VIM::has?('syntax')
+          ::VIM::command "highlight link CommandTSelection #{@highlight_color}"
+        end
+      end
+    end
+
+    def find(char)
+      # is this a new search or the continuation of a previous one?
+      now = Time.now
+      if @last_key_time.nil? || @last_key_time < (now - 0.5)
+        @find_string = char
+      else
+        @find_string += char
+      end
+      @last_key_time = now
+
+      # see if there's anything up ahead that matches
+      matches = @reverse_list ? @matches.reverse : @matches
+      matches.each_with_index do |match, idx|
+        if match[0, @find_string.length].casecmp(@find_string) == 0
+          old_selection = @selection
+          @selection = @reverse_list ? matches.length - idx - 1 : idx
+          print_match(old_selection)  # redraw old selection (removes marker)
+          print_match(@selection)     # redraw new selection (adds marker)
+          break
+        end
+      end
+    end
+
+    # Returns the currently selected item as a String.
+    def selection
+      @matches[@selection]
+    end
+
+    def print_no_such_file_or_directory
+      print_error 'NO SUCH FILE OR DIRECTORY'
+    end
+
+  private
+
+    def focus_window(number)
+      ::VIM::command("noautocmd #{number}wincmd w")
+    end
+
+    def _next
+      if @selection < [@window.height, @matches.length].min - 1
+        @selection += 1
+        print_match(@selection - 1) # redraw old selection (removes marker)
+        print_match(@selection)     # redraw new selection (adds marker)
+        move_cursor_to_selected_line
+      end
+    end
+
+    def _prev
+      if @selection > 0
+        @selection -= 1
+        print_match(@selection + 1) # redraw old selection (removes marker)
+        print_match(@selection)     # redraw new selection (adds marker)
+        move_cursor_to_selected_line
+      end
+    end
+
+    # Translate from a 0-indexed match index to a 1-indexed Vim line number.
+    # Also takes into account reversed listings.
+    def line(match_index)
+      @reverse_list ? @window.height - match_index : match_index + 1
+    end
+
+    def set(setting, value)
+      @settings ||= Settings.new
+      @settings.set(setting, value)
+    end
+
+    def move_cursor_to_selected_line
+      # on some non-GUI terminals, the cursor doesn't hide properly
+      # so we move the cursor to prevent it from blinking away in the
+      # upper-left corner in a distracting fashion
+      @window.cursor = [line(@selection), 0]
+    end
+
+    def print_error(msg)
+      return unless VIM::Window.select(@window)
+      unlock
+      clear
+      @window.height = [1, @min_height].min
+      @@buffer[1] = "-- #{msg} --"
+      lock
+    end
+
+    def restore_window_views
+      # sort from tallest to shortest, tie-breaking on window width
+      @windows.sort! do |a, b|
+        order = b.height <=> a.height
+        if order.zero?
+          b.width <=> a.width
+        else
+          order
+        end
+      end
+
+      # starting with the tallest ensures that there are no constraints
+      # preventing windows on the side of vertical splits from regaining
+      # their original full size
+      current_window = ::VIM::evaluate('winnr()')
+      @windows.each do |w|
+        # beware: window may be nil
+        if window = ::VIM::Window[w.index]
+          window.height = w.height
+          window.width  = w.width
+          focus_window(w.index + 1)
+          ::VIM::evaluate("winrestview({" +
+            "'lnum': #{w.lnum}," +
+            "'col': #{w.col}, " +
+            "'coladd': #{w.coladd}, " +
+            "'curswant': #{w.curswant}, " +
+            "'topline': #{w.topline}, " +
+            "'topfill': #{w.topfill}, " +
+            "'leftcol': #{w.leftcol}, " +
+            "'skipcol': #{w.skipcol}" +
+          "})")
+        end
+      end
+      focus_window(current_window)
+    end
+
+    def run_will_show_autocmd
+      run_autocmd('CommandTWillShowMatchListing')
+    end
+
+    def run_did_hide_autocmd
+      run_autocmd('CommandTDidHideMatchListing')
+    end
+
+    def run_autocmd(cmd)
+      ::VIM::command("call commandt#private#RunAutocmd('#{cmd}')")
+    end
+
+    def match_text_for_idx(idx)
+      match = truncated_match @matches[idx].to_s
+      if idx == @selection
+        prefix = SELECTION_MARKER
+        suffix = padding_for_selected_match match
+      else
+        if VIM::has?('syntax') && VIM::has?('conceal')
+          match = match_with_syntax_highlight match
+        end
+        prefix = UNSELECTED_MARKER
+        suffix = ''
+      end
+      prefix + match + suffix
+    end
+
+    # Highlight matching characters within the matched string.
+    #
+    # Note that this is only approximate; it will highlight the first matching
+    # instances within the string, which may not actually be the instances that
+    # were used by the matching/scoring algorithm to determine the best score
+    # for the match.
+    #
+    def match_with_syntax_highlight(match)
+      highlight_chars = @prompt.abbrev.downcase.scan(/./mu)
+      if @encoding &&
+         match.respond_to?(:force_encoding) &&
+         match.encoding != @encoding
+        match = match.force_encoding(@encoding)
+      end
+      match.scan(/./mu).inject([]) do |output, char|
+        if char.downcase == highlight_chars.first
+          highlight_chars.shift
+          output.concat [MH_START, char, MH_END]
+        else
+          output << char
+        end
+      end.join
+    end
+
+    # Print just the specified match.
+    def print_match(idx)
+      return unless VIM::Window.select(@window)
+      unlock
+      @@buffer[line(idx)] = match_text_for_idx idx
+      lock
+    end
+
+    def max_lines
+      [1, VIM::Screen.lines - 5].max
+    end
+
+    # Print all matches.
+    def print_matches
+      match_count = @matches.length
+      if match_count == 0
+        print_error 'NO MATCHES'
+      else
+        return unless VIM::Window.select(@window)
+        unlock
+        clear
+        @window_width = @window.width # update cached value
+        desired_lines = [match_count, @min_height].max
+        desired_lines = [max_lines, desired_lines].min
+        @window.height = desired_lines
+        matches = []
+        (0...@window.height).each do |idx|
+          text = match_text_for_idx(idx)
+          @reverse_list ? matches.unshift(text) : matches.push(text)
+        end
+        matches.each_with_index do |match, idx|
+          if @@buffer.count > idx
+            @@buffer[idx + 1] = match
+          else
+            @@buffer.append(idx, match)
+          end
+        end
+        lock
+      end
+    end
+
+    # Prepare padding for match text (trailing spaces) so that selection
+    # highlighting extends all the way to the right edge of the window.
+    def padding_for_selected_match(str)
+      len = str.length
+      if len >= @window_width - MARKER_LENGTH
+        ''
+      else
+        ' ' * (@window_width - MARKER_LENGTH - len)
+      end
+    end
+
+    # Convert "really/long/path" into "really...path" based on available
+    # window width.
+    def truncated_match(str)
+      len = str.length
+      available_width = @window_width - MARKER_LENGTH
+      return str if len <= available_width
+      left = (available_width / 2) - 1
+      right = (available_width / 2) - 2 + (available_width % 2)
+      str[0, left] + '...' + str[-right, right]
+    end
+
+    def clear
+      # range = % (whole buffer)
+      # action = d (delete)
+      # register = _ (black hole register, don't record deleted text)
+      ::VIM::command 'silent %d _'
+    end
+
+    def get_cursor_highlight
+      # there are 4 possible formats to check for, each needing to be
+      # transformed in a certain way in order to reapply the highlight:
+      #   Cursor xxx guifg=bg guibg=fg      -> :hi! Cursor guifg=bg guibg=fg
+      #   Cursor xxx links to SomethingElse -> :hi! link Cursor SomethingElse
+      #   Cursor xxx [definition]
+      #              links to VisualNOS     -> both of the above
+      #   Cursor xxx cleared                -> :hi! clear Cursor
+      highlight = VIM::capture 'silent! 0verbose highlight Cursor'
+
+      if highlight =~ /^Cursor\s+xxx\s+(.+)\blinks to (\w+)/m
+        [
+          Highlight.new("Cursor #{$~[1]}"),
+          Highlight.new("link Cursor #{$~[2]}", '!')
+        ]
+      elsif highlight =~ /^Cursor\s+xxx\s+links to (\w+)/m
+        [Highlight.new("link Cursor #{$~[1]}")]
+      elsif highlight =~ /^Cursor\s+xxx\s+cleared/m
+        [Highlight.new('clear Cursor')]
+      elsif highlight =~ /Cursor\s+xxx\s+(.+)/m
+        [Highlight.new("Cursor #{$~[1]}")]
+      else # likely cause E411 Cursor highlight group not found
+        []
+      end
+    end
+
+    def hide_cursor
+      if @cursor_highlight
+        ::VIM::command 'highlight Cursor NONE'
+      end
+    end
+
+    def show_cursor
+      if @cursor_highlight
+        @cursor_highlight.each do |highlight|
+          config = highlight.highlight.gsub(/\s+/, ' ')
+          ::VIM::command "highlight#{highlight.bang} #{config}"
+        end
+      end
+    end
+
+    def lock
+      set 'modifiable', false
+    end
+
+    def unlock
+      set 'modifiable', true
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/metadata/fallback.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/metadata/fallback.rb
new file mode 100644 (file)
index 0000000..00d0aa0
--- /dev/null
@@ -0,0 +1,12 @@
+# Copyright 2015-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  module Metadata
+    # This file gets overwritten with actual data during the installation
+    # process (when `ruby extconf.rb` is run).
+    EXPECTED_RUBY_VERSION = '[unknown]'
+    EXPECTED_RUBY_PATCHLEVEL = '[unknown]'
+    UNKNOWN = true
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/mru.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/mru.rb
new file mode 100644 (file)
index 0000000..68dd140
--- /dev/null
@@ -0,0 +1,43 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  # Maintains a stack of seen buffers in MRU (most recently used) order.
+  module MRU
+    class << self
+      # The stack of used buffers in MRU order.
+      def stack
+        @stack ||= []
+      end
+
+      # The (last) most recent buffer in the stack, if any.
+      def last
+        stack.last
+      end
+
+      # Mark the current buffer as having been used, effectively moving it to
+      # the top of the stack.
+      def touch
+        return unless ::VIM::evaluate('buflisted(%d)' % $curbuf.number) == 1
+        return unless $curbuf.name
+
+        stack.delete $curbuf
+        stack.push $curbuf
+      end
+
+      # Mark a buffer as deleted, removing it from the stack.
+      def delete
+        # Note that $curbuf does not point to the buffer that is being deleted;
+        # we need to use Vim's <abuf> for the correct buffer number.
+        stack.delete_if do |b|
+          b.number == ::VIM::evaluate('expand("<abuf>")').to_i
+        end
+      end
+
+      # Returns `true` if `buffer` has been used (ie. is present in the stack).
+      def used?(buffer)
+        stack.include?(buffer)
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/path_utilities.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/path_utilities.rb
new file mode 100644 (file)
index 0000000..8e77209
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  module PathUtilities
+
+  private
+
+    def relative_path_under_working_directory(path)
+      # any path under the working directory will be specified as a relative
+      # path to improve the readability of the buffer list etc
+      pwd = File.expand_path(VIM::pwd) + '/'
+      path.index(pwd) == 0 ? path[pwd.length..-1] : path
+    end
+
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/progress_reporter.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/progress_reporter.rb
new file mode 100644 (file)
index 0000000..efa74c9
--- /dev/null
@@ -0,0 +1,38 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  # Simple class for displaying scan progress to the user.
+  #
+  # The active scanner calls the `#update` method with a `count` to inform it of
+  # progress, the reporter updates the UI and then returns a suggested count at
+  # which to invoke `#update` again in the future (the suggested count is based
+  # on a heuristic that seeks to update the UI about 5 times per second).
+  class ProgressReporter
+    SPINNER = %w[^ > v <]
+
+    def initialize
+      @spinner ||= SPINNER.first
+    end
+
+    def update(count)
+      @spinner = SPINNER[(SPINNER.index(@spinner) + 1) % SPINNER.length]
+
+      ::VIM::command "echon '#{@spinner}  #{count}'"
+      ::VIM::command 'redraw'
+
+      # Aim for 5 updates per second.
+      now = Time.now.to_f
+      if @last_time
+        time_diff = now - @last_time
+        count_diff = count - @last_count
+        next_count = count + ((0.2 / time_diff) * count_diff).to_i
+      else
+        next_count = count + 100
+      end
+      @last_time = now
+      @last_count = count
+      next_count
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/prompt.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/prompt.rb
new file mode 100644 (file)
index 0000000..0450ff9
--- /dev/null
@@ -0,0 +1,162 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  # Abuse the status line as a prompt.
+  class Prompt
+    attr_accessor :abbrev
+
+    def initialize(options = {})
+      @abbrev       = ''  # abbreviation entered so far
+      @col          = 0   # cursor position
+      @cursor_color = options[:cursor_color] || 'Underlined'
+      @has_focus    = false
+    end
+
+    # Erase whatever is displayed in the prompt line,
+    # effectively disposing of the prompt
+    def dispose
+      ::VIM::command 'echo'
+      ::VIM::command 'redraw'
+    end
+
+    # Clear any entered text.
+    def clear!
+      @abbrev = ''
+      @col    = 0
+      redraw
+    end
+
+    # Remove word before cursor
+    def clear_prev_word!
+      suffix_length = @abbrev.length - @col
+      @abbrev.match(
+        %r{
+          (.*?)                 # prefix
+          \w*\s*                # word to clear
+          (.{#{suffix_length}}) # suffix
+          \z
+        }x
+      )
+      @abbrev = $~[1] + $~[2]
+      @col = @abbrev.length - suffix_length
+      redraw
+    end
+
+    # Insert a character at (before) the current cursor position.
+    def add!(char)
+      left, cursor, right = abbrev_segments
+      @abbrev = left + char + cursor + right
+      @col += 1
+      redraw
+    end
+
+    # Delete a character to the left of the current cursor position.
+    def backspace!
+      if @col > 0
+        left, cursor, right = abbrev_segments
+        @abbrev = left.chop! + cursor + right
+        @col -= 1
+        redraw
+      end
+    end
+
+    # Delete a character at the current cursor position.
+    def delete!
+      if @col < @abbrev.length
+        left, cursor, right = abbrev_segments
+        @abbrev = left + right
+        redraw
+      end
+    end
+
+    def cursor_left
+      if @col > 0
+        @col -= 1
+        redraw
+      end
+    end
+
+    def cursor_right
+      if @col < @abbrev.length
+        @col += 1
+        redraw
+      end
+    end
+
+    def cursor_end
+      if @col < @abbrev.length
+        @col = @abbrev.length
+        redraw
+      end
+    end
+
+    def cursor_start
+      if @col != 0
+        @col = 0
+        redraw
+      end
+    end
+
+    def focus
+      unless @has_focus
+        @has_focus = true
+        redraw
+      end
+    end
+
+    def unfocus
+      if @has_focus
+        @has_focus = false
+        redraw
+      end
+    end
+
+    def redraw
+      if @has_focus
+        prompt_highlight = 'Comment'
+        normal_highlight = 'None'
+        cursor_highlight = @cursor_color
+      else
+        prompt_highlight = 'NonText'
+        normal_highlight = 'NonText'
+        cursor_highlight = 'NonText'
+      end
+      left, cursor, right = abbrev_segments
+      components = [prompt_highlight, '>>', 'None', ' ']
+      components += [normal_highlight, left] unless left.empty?
+      components += [cursor_highlight, cursor] unless cursor.empty?
+      components += [normal_highlight, right] unless right.empty?
+      components += [cursor_highlight, ' '] if cursor.empty?
+      set_status *components
+    end
+
+  private
+
+    # Returns the @abbrev string divided up into three sections, any of
+    # which may actually be zero width, depending on the location of the
+    # cursor:
+    #   - left segment (to left of cursor)
+    #   - cursor segment (character at cursor)
+    #   - right segment (to right of cursor)
+    def abbrev_segments
+      left    = @abbrev[0, @col]
+      cursor  = @abbrev[@col, 1]
+      right   = @abbrev[(@col + 1)..-1] || ''
+      [left, cursor, right]
+    end
+
+    def set_status(*args)
+      # see ':help :echo' for why forcing a redraw here helps
+      # prevent the status line from getting inadvertantly cleared
+      # after our echo commands
+      ::VIM::command 'redraw'
+      while (highlight = args.shift) && (text = args.shift)
+        text = VIM::escape_for_single_quotes text
+        ::VIM::command "echohl #{highlight}"
+        ::VIM::command "echon '#{text}'"
+      end
+      ::VIM::command 'echohl None'
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner.rb
new file mode 100644 (file)
index 0000000..29f6fd2
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    autoload :BufferScanner,    'command-t/scanner/buffer_scanner'
+    autoload :CommandScanner,   'command-t/scanner/command_scanner'
+    autoload :FileScanner,      'command-t/scanner/file_scanner'
+    autoload :HelpScanner,      'command-t/scanner/help_scanner'
+    autoload :HistoryScanner,   'command-t/scanner/history_scanner'
+    autoload :JumpScanner,      'command-t/scanner/jump_scanner'
+    autoload :LineScanner,      'command-t/scanner/line_scanner'
+    autoload :MRUBufferScanner, 'command-t/scanner/mru_buffer_scanner'
+    autoload :TagScanner,       'command-t/scanner/tag_scanner'
+
+    # Subclasses implement this method to return the list of paths that should
+    # be searched.
+    #
+    # Note that as an optimization, the C extension will record the
+    # `Object#object_id` of the returned array and assumes it will not be
+    # mutated.
+    def paths
+      raise RuntimeError, 'Subclass responsibility'
+    end
+
+  private
+
+    def progress_reporter
+      @progress_reporter ||= ProgressReporter.new
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/buffer_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/buffer_scanner.rb
new file mode 100644 (file)
index 0000000..9933a71
--- /dev/null
@@ -0,0 +1,20 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    # Returns a list of all open buffers.
+    class BufferScanner < Scanner
+      include PathUtilities
+
+      def paths
+        (0..(::VIM::Buffer.count - 1)).map do |n|
+          buffer = ::VIM::Buffer[n]
+          if buffer.name # beware, may be nil
+            relative_path_under_working_directory buffer.name
+          end
+        end.compact
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/command_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/command_scanner.rb
new file mode 100644 (file)
index 0000000..17195c0
--- /dev/null
@@ -0,0 +1,33 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class CommandScanner < Scanner
+      def paths
+        @paths ||= paths!
+      end
+
+    private
+
+      def paths!
+        # Get user commands.
+        commands = VIM.capture('silent command').split("\n")[2..-1].map do |line|
+          line.sub(/\A.{4}(\S+).+/, '\1')
+        end
+
+        # Get built-in commands from `ex-cmd-index`.
+        ex_cmd_index = ::VIM.evaluate('expand(findfile("doc/index.txt", &runtimepath))')
+        if File.readable?(ex_cmd_index)
+          File.readlines(ex_cmd_index).each do |line|
+            if line =~ %r{\A\|:([^|]+)\|\s+}
+              commands << $~[1]
+            end
+          end
+        end
+
+        commands.uniq
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner.rb
new file mode 100644 (file)
index 0000000..b4cc11b
--- /dev/null
@@ -0,0 +1,88 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    # Reads the current directory recursively for the paths to all regular files.
+    #
+    # This is an abstract superclass; the real work is done by subclasses which
+    # obtain file listings via different strategies (for examples, see the
+    # RubyFileScanner and FindFileScanner subclasses).
+    class FileScanner < Scanner
+      # Subclasses
+      autoload :FindFileScanner,     'command-t/scanner/file_scanner/find_file_scanner'
+      autoload :GitFileScanner,      'command-t/scanner/file_scanner/git_file_scanner'
+      autoload :RubyFileScanner,     'command-t/scanner/file_scanner/ruby_file_scanner'
+      autoload :WatchmanFileScanner, 'command-t/scanner/file_scanner/watchman_file_scanner'
+
+      attr_accessor :path
+
+      def initialize(path = Dir.pwd, options = {})
+        @paths                = {}
+        @paths_keys           = []
+        @path                 = path
+        @max_depth            = options[:max_depth] || 15
+        @max_files            = options[:max_files] || 100_000
+        @max_caches           = options[:max_caches] || 1
+        @scan_dot_directories = options[:scan_dot_directories] || false
+        @wildignore           = options[:wildignore]
+        @scan_submodules      = options[:git_scan_submodules] || false
+      end
+
+      def paths
+        @paths[@path] ||= begin
+          ensure_cache_under_limit
+          @prefix_len = @path.chomp('/').length + 1
+          paths!
+        end
+      end
+
+      def flush
+        @paths = {}
+      end
+
+    private
+
+      def show_max_files_warning
+        unless VIM::get_bool('g:CommandTSuppressMaxFilesWarning', false)
+          ::VIM::command('redraw!')
+          ::VIM::command('echohl ErrorMsg')
+          warning =
+            "Warning: maximum file limit reached\n" +
+            "\n" +
+            "Increase it by setting a higher value in $MYVIMRC; eg:\n" +
+            "  let g:CommandTMaxFiles=#{@max_files * 2}\n" +
+            "Or suppress this warning by setting:\n" +
+            "  let g:CommandTSuppressMaxFilesWarning=1\n" +
+            "For best performance, consider using a fast scanner; see:\n" +
+            "  :help g:CommandTFileScanner\n" +
+            "\n" +
+            "Press ENTER to continue."
+          ::VIM::evaluate(%{input("#{warning}")})
+          ::VIM::command('echohl None')
+        end
+      end
+
+      def paths!
+        raise RuntimeError, 'Subclass responsibility'
+      end
+
+      def ensure_cache_under_limit
+        # Ruby 1.8 doesn't have an ordered hash, so use a separate stack to
+        # track and expire the oldest entry in the cache
+        if @max_caches > 0 && @paths_keys.length >= @max_caches
+          @paths.delete @paths_keys.shift
+        end
+        @paths_keys << @path
+      end
+
+      def path_excluded?(path, prefix_len = @prefix_len)
+        if @wildignore
+          # First strip common prefix (@path) from path to match Vim's behavior.
+          path = path[prefix_len..-1]
+          path =~ @wildignore
+        end
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/find_file_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/find_file_scanner.rb
new file mode 100644 (file)
index 0000000..d22ed69
--- /dev/null
@@ -0,0 +1,55 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'open3'
+
+module CommandT
+  class Scanner
+    class FileScanner
+      # A FileScanner which shells out to the `find` executable in order to scan.
+      class FindFileScanner < FileScanner
+        include PathUtilities
+
+        def paths!
+          # temporarily set field separator to NUL byte; this setting is
+          # respected by both `each_line` and `chomp!` below, and makes it easier
+          # to parse the output of `find -print0`
+          separator = $/
+          $/ = "\x00"
+
+          unless @scan_dot_directories
+            dot_directory_filter = [
+              '-not', '-path', "#{@path}/.*/*",           # top-level dot dir
+              '-and', '-not', '-path', "#{@path}/*/.*/*"  # lower-level dot dir
+            ]
+          end
+
+          paths = []
+          Open3.popen3(*([
+            'find', '-L',                 # follow symlinks
+            @path,                        # anchor search here
+            '-maxdepth', @max_depth.to_s, # limit depth of DFS
+            '-type', 'f',                 # only show regular files (not dirs etc)
+            dot_directory_filter,         # possibly skip out dot directories
+            '-print0'                     # NUL-terminate results
+          ].flatten.compact)) do |stdin, stdout, stderr|
+            counter = 1
+            next_progress = progress_reporter.update(counter)
+            stdout.each_line do |line|
+              next if path_excluded?(line.chomp!)
+              paths << line[@prefix_len..-1]
+              next_progress = progress_reporter.update(counter) if counter == next_progress
+              if (counter += 1) > @max_files
+                show_max_files_warning
+                break
+              end
+            end
+          end
+          paths
+        ensure
+          $/ = separator
+        end
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/git_file_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/git_file_scanner.rb
new file mode 100644 (file)
index 0000000..8306833
--- /dev/null
@@ -0,0 +1,57 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class FileScanner
+      # Uses git ls-files to scan for files
+      class GitFileScanner < FindFileScanner
+        LsFilesError = Class.new(::RuntimeError)
+
+        def paths!
+          Dir.chdir(@path) do
+            all_files = list_files(%w[git ls-files --exclude-standard -z])
+
+            if @scan_submodules
+              base = nil
+              list_files(%w[
+                git submodule foreach --recursive
+                git ls-files --exclude-standard -z
+              ]).each do |path|
+                if path =~ /\AEntering '(.*)'\n(.*)\z/
+                  base = $~[1]
+                  path = $~[2]
+                end
+                all_files.push(base + File::SEPARATOR + path)
+              end
+            end
+
+            filtered = all_files.
+              map { |path| path.chomp }.
+              reject { |path| path_excluded?(path, 0) }
+            truncated = filtered.take(@max_files)
+            if truncated.count < filtered.count
+              show_max_files_warning
+            end
+            truncated.to_a
+          end
+        rescue LsFilesError
+          super
+        rescue Errno::ENOENT
+          # git executable not present and executable
+          super
+        end
+
+      private
+
+        def list_files(command)
+          stdin, stdout, stderr = Open3.popen3(*command)
+          stdout.read.split("\0")
+        ensure
+          raise LsFilesError if stderr && stderr.gets
+        end
+
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/ruby_file_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/ruby_file_scanner.rb
new file mode 100644 (file)
index 0000000..0317a43
--- /dev/null
@@ -0,0 +1,60 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class FileScanner
+      FileLimitExceeded = Class.new(::RuntimeError)
+
+      # Pure Ruby implementation of a file scanner.
+      class RubyFileScanner < FileScanner
+        def paths!
+          accumulator = []
+          @depth = 0
+          @files = 0
+          @next_progress = progress_reporter.update(@files)
+          add_paths_for_directory(@path, accumulator)
+          accumulator
+        rescue FileLimitExceeded
+          show_max_files_warning
+          accumulator
+        end
+
+      private
+
+        def looped_symlink?(path)
+          if File.symlink?(path)
+            target = File.expand_path(File.readlink(path), File.dirname(path))
+            target.include?(@path) || @path.include?(target)
+          end
+        end
+
+        def add_paths_for_directory(dir, accumulator)
+          Dir.foreach(dir) do |entry|
+            next if ['.', '..'].include?(entry)
+            path = File.join(dir, entry)
+            unless path_excluded?(path)
+              if File.file?(path)
+                @files += 1
+                @next_progress = progress_reporter.update(@files) if @files == @next_progress
+                raise FileLimitExceeded if @files > @max_files
+                accumulator << path[@prefix_len..-1]
+              elsif File.directory?(path)
+                next if @depth >= @max_depth
+                next if (entry.match(/\A\./) && !@scan_dot_directories)
+                next if looped_symlink?(path)
+                @depth += 1
+                add_paths_for_directory(path, accumulator)
+                @depth -= 1
+              end
+            end
+          end
+        rescue Errno::EACCES
+          # skip over directories for which we don't have access
+        rescue ArgumentError
+          # skip over bad file names
+        end
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/watchman_file_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/file_scanner/watchman_file_scanner.rb
new file mode 100644 (file)
index 0000000..a275382
--- /dev/null
@@ -0,0 +1,72 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'pathname'
+require 'socket'
+
+module CommandT
+  class Scanner
+    class FileScanner
+      # A FileScanner which delegates the heavy lifting to Watchman
+      # (https://github.com/facebook/watchman); useful for very large hierarchies.
+      #
+      # Inherits from FindFileScanner so that it can fall back to it in the event
+      # that Watchman isn't available or able to fulfil the request.
+      class WatchmanFileScanner < FindFileScanner
+        # Exception raised when Watchman is unavailable or unable to process the
+        # requested path.
+        WatchmanError = Class.new(::RuntimeError)
+
+        def paths!
+          sockname = extract_value(
+            Watchman::Utils.load(get_raw_sockname),
+            'sockname'
+          )
+
+          UNIXSocket.open(sockname) do |socket|
+            root = Pathname.new(@path).realpath.to_s
+            roots = Watchman::Utils.query(['watch-list'], socket)['roots']
+            if !roots.include?(root)
+              # this path isn't being watched yet; try to set up watch
+              result = Watchman::Utils.query(['watch', root], socket)
+
+              # root_restrict_files setting may prevent Watchman from working
+              # or enforce_root_files/root_files (>= version 3.1)
+              extract_value(result)
+            end
+
+            query = ['query', root, {
+              'expression' => ['type', 'f'],
+              'fields'     => ['name'],
+            }]
+            paths = Watchman::Utils.query(query, socket)
+
+            # could return error if watch is removed
+            extracted = extract_value(paths, 'files')
+            if @wildignore
+              extracted.select { |path| path !~ @wildignore }
+            else
+              extracted
+            end
+          end
+        rescue Errno::ENOENT, WatchmanError
+          # watchman executable not present, or unable to fulfil request
+          super
+        end
+
+      private
+
+        def extract_value(object, key = nil)
+          raise WatchmanError, object['error'] if object.has_key?('error')
+          object[key]
+        end
+
+        def get_raw_sockname
+          raw_sockname = %x{watchman --output-encoding=bser get-sockname}
+          raise WatchmanError, 'get-sockname failed' if !$?.exitstatus.zero?
+          raw_sockname
+        end
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/help_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/help_scanner.rb
new file mode 100644 (file)
index 0000000..26f834c
--- /dev/null
@@ -0,0 +1,40 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class HelpScanner < Scanner
+      def paths
+        @cached_tags ||= paths!
+      end
+
+      def flush
+        @cached_tags = nil
+      end
+
+    private
+
+      def paths!
+        # Vim doesn't provide an easy way to get a list of all help tags.
+        # `tagfiles()` only shows the tagfiles for the current buffer, so you
+        # need to already be in a buffer of `'buftype'` `help` for that to work.
+        # Likewise, `taglist()` only shows tags that apply to the current file
+        # type, and `:tag` has the same restriction.
+        #
+        # So, we look for a "doc/tags" file at every location in the
+        # `'runtimepath'` and try to manually parse it.
+        tags = []
+
+        ::VIM::evaluate('findfile("doc/tags", &runtimepath, -1)').each do |path|
+          if File.readable?(path)
+            File.readlines(path).each do |tag|
+              tags << tag.split.first if tag.split.first
+            end
+          end
+        end
+
+        tags
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/history_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/history_scanner.rb
new file mode 100644 (file)
index 0000000..4f33646
--- /dev/null
@@ -0,0 +1,24 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class HistoryScanner < Scanner
+      def initialize(history_command)
+        @history_command = history_command
+      end
+
+      def paths
+        @paths ||= paths!
+      end
+
+    private
+
+      def paths!
+        VIM.capture(@history_command).split("\n")[2..-1].map do |line|
+          line.sub(/\A>?\s*\d+\s*(.+)/, '\1').strip
+        end.uniq
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/jump_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/jump_scanner.rb
new file mode 100644 (file)
index 0000000..e4535ec
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    # Returns a list of files in the jumplist.
+    class JumpScanner < Scanner
+      include PathUtilities
+
+      def paths
+        jumps_with_filename = jumps.lines.select do |line|
+          line_contains_filename?(line)
+        end
+        filenames = jumps_with_filename[1..-2].map do |line|
+          relative_path_under_working_directory line.split[3]
+        end
+
+        filenames.sort.uniq
+      end
+
+    private
+
+      def line_contains_filename?(line)
+        line.split.count > 3
+      end
+
+      def jumps
+        VIM::capture 'silent jumps'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/line_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/line_scanner.rb
new file mode 100644 (file)
index 0000000..f243fe8
--- /dev/null
@@ -0,0 +1,45 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class LineScanner < Scanner
+      def paths
+        @lines ||= paths!
+      end
+
+    private
+
+      def paths!
+        # $curbuf is the Command-T match listing; we actually want the last
+        # buffer, but passing `$`, `#`, `%` etc to `bufnr()` returns the wrong
+        # value.
+        number = ::VIM.evaluate("g:CommandTCurrentBuffer").to_i
+        return [] unless number > 0
+        buffer = nil
+        (0...(::VIM::Buffer.count)).each do |n|
+          buffer = ::VIM::Buffer[n]
+          if buffer_number(buffer) == number
+            break
+          else
+            buffer = nil
+          end
+        end
+        return [] unless buffer
+
+        (1..(buffer.length)).map do |n|
+          line = buffer[n]
+          unless line.match(/\A\s*\z/)
+            line.sub(/\A\s*/, '') + ':' + n.to_s
+          end
+        end.compact
+      end
+
+      def buffer_number(buffer)
+        buffer && buffer.number
+      rescue Vim::DeletedBufferError
+        # Beware of people manually deleting Command-T's hidden, unlisted buffer.
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/mru_buffer_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/mru_buffer_scanner.rb
new file mode 100644 (file)
index 0000000..9931ed3
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    # Returns a list of all open buffers, sorted in MRU order.
+    class MRUBufferScanner < BufferScanner
+      include PathUtilities
+
+      def paths
+        # Collect all buffers that have not been used yet.
+        unused_buffers = (0..(::VIM::Buffer.count - 1)).map do |n|
+          buffer = ::VIM::Buffer[n]
+          buffer if buffer.name && !MRU.used?(buffer)
+        end
+
+        # Combine all most recently used buffers and all unused buffers, and
+        # return all listed buffer paths.
+        (unused_buffers + MRU.stack).map do |buffer|
+          if buffer && buffer.name
+            relative_path_under_working_directory buffer.name
+          end
+        end.compact.reverse
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/tag_scanner.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scanner/tag_scanner.rb
new file mode 100644 (file)
index 0000000..e7a3bb0
--- /dev/null
@@ -0,0 +1,33 @@
+# Copyright 2011-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Scanner
+    class TagScanner < Scanner
+      attr_reader :include_filenames
+
+      def initialize(options = {})
+        @include_filenames = options[:include_filenames] || false
+        @cached_tags = nil
+      end
+
+      def paths
+        @cached_tags ||= taglist.map do |tag|
+          path = tag['name']
+          path << ":#{tag['filename']}" if @include_filenames
+          path
+        end.uniq.sort
+      end
+
+      def flush
+        @cached_tags = nil
+      end
+
+    private
+
+      def taglist
+        ::VIM::evaluate 'taglist(".")'
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scm_utilities.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/scm_utilities.rb
new file mode 100644 (file)
index 0000000..e92a8c0
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  module SCMUtilities
+
+  private
+
+    def nearest_ancestor(starting_directory, markers)
+      path = File.expand_path(starting_directory)
+      while !markers.
+        map { |dir| File.join(path, dir) }.
+        map { |dir| File.exist?(dir) }.
+        any?
+        next_path = File.expand_path(File.join(path, '..'))
+        return nil if next_path == path
+        path = next_path
+      end
+      path
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/settings.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/settings.rb
new file mode 100644 (file)
index 0000000..1777bc1
--- /dev/null
@@ -0,0 +1,99 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  # Convenience class for saving and restoring global settings.
+  class Settings
+    # Settings which apply globally and so must be manually saved and restored
+    GLOBAL_SETTINGS = %w[
+      equalalways
+      hlsearch
+      insertmode
+      report
+      showcmd
+      scrolloff
+      sidescroll
+      sidescrolloff
+      timeout
+      timeoutlen
+      updatetime
+    ]
+
+    # Settings which can be made locally to the Command-T buffer or window
+    LOCAL_SETTINGS = %w[
+      bufhidden
+      buflisted
+      buftype
+      colorcolumn
+      concealcursor
+      conceallevel
+      cursorline
+      filetype
+      foldcolumn
+      foldlevel
+      list
+      modifiable
+      number
+      readonly
+      relativenumber
+      spell
+      swapfile
+      synmaxcol
+      textwidth
+      wrap
+    ]
+
+    KNOWN_SETTINGS = GLOBAL_SETTINGS + LOCAL_SETTINGS
+
+    def initialize
+      @settings = []
+    end
+
+    def set(setting, value)
+      raise "Unknown setting #{setting}" unless KNOWN_SETTINGS.include?(setting)
+
+      case value
+      when TrueClass, FalseClass
+        @settings.push([setting, VIM::get_bool("&#{setting}")]) if global?(setting)
+        set_bool setting, value
+      when Numeric
+        @settings.push([setting, VIM::get_number("&#{setting}")]) if global?(setting)
+        set_number setting, value
+      when String
+        @settings.push([setting, VIM::get_string("&#{setting}")]) if global?(setting)
+        set_string setting, value
+      end
+    end
+
+    def restore
+      @settings.each do |setting, value|
+        case value
+        when TrueClass, FalseClass
+          set_bool setting, value
+        when Numeric
+          set_number setting, value
+        when String
+          set_string setting, value
+        end
+      end
+    end
+
+  private
+
+    def global?(setting)
+      GLOBAL_SETTINGS.include?(setting)
+    end
+
+    def set_bool(setting, value)
+      command = global?(setting) ? 'set' : 'setlocal'
+      setting = value ? setting : "no#{setting}"
+      ::VIM::command "#{command} #{setting}"
+    end
+
+    def set_number(setting, value)
+      command = global?(setting) ? 'set' : 'setlocal'
+      ::VIM::command "#{command} #{setting}=#{value}"
+    end
+    alias set_string set_number
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/stub.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/stub.rb
new file mode 100644 (file)
index 0000000..7126303
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  class Stub
+    @@expected_version = Metadata::EXPECTED_RUBY_VERSION
+    @@expected_patchlevel = Metadata::EXPECTED_RUBY_PATCHLEVEL
+    @@patch_level = defined?(RUBY_PATCHLEVEL) ? RUBY_PATCHLEVEL : '[unknown]'
+    @@load_error = ['command-t.vim could not load the C extension',
+                    'Please see INSTALLATION and TROUBLE-SHOOTING in the help',
+                    "Vim Ruby version: #{RUBY_VERSION}-p#{@@patch_level}",
+                    "Expected version: #{@@expected_version}-p#{@@expected_patchlevel}",
+                    'For more information type:    :help command-t']
+
+    [
+      :flush,
+      :show_buffer_finder,
+      :show_command_finder,
+      :show_file_finder,
+      :show_history_finder,
+      :show_help_finder,
+      :show_jump_finder,
+      :show_line_finder,
+      :show_mru_finder,
+      :show_search_finder,
+      :show_tag_finder
+    ].each do |method|
+      define_method(method) { warn *@@load_error }
+    end
+
+  private
+
+    def warn(*msg)
+      ::VIM::command 'echohl WarningMsg'
+      msg.each { |m| ::VIM::command "echo '#{m}'" }
+      ::VIM::command 'echohl none'
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/util.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/util.rb
new file mode 100644 (file)
index 0000000..471e857
--- /dev/null
@@ -0,0 +1,94 @@
+# Copyright 2013-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'rbconfig'
+
+module CommandT
+  module Util
+    class << self
+      def processor_count
+        @processor_count ||= begin
+          count = processor_count!
+          count = 1 if count < 1   # sanity check
+          count = 32 if count > 32 # sanity check
+          count
+        end
+      end
+
+    private
+
+      # This method derived from:
+      #
+      #   https://github.com/grosser/parallel/blob/d11e4a3c8c1a/lib/parallel.rb
+      #
+      # Number of processors seen by the OS and used for process scheduling.
+      #
+      # * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev
+      # * BSD: /sbin/sysctl
+      # * Cygwin: /proc/cpuinfo
+      # * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl
+      # * HP-UX: /usr/sbin/ioscan
+      # * IRIX: /usr/sbin/sysconf
+      # * Linux: /proc/cpuinfo
+      # * Minix 3+: /proc/cpuinfo
+      # * Solaris: /usr/sbin/psrinfo
+      # * Tru64 UNIX: /usr/sbin/psrinfo
+      # * UnixWare: /usr/sbin/psrinfo
+      #
+      # Copyright (C) 2013 Michael Grosser <michael@grosser.it>
+      #
+      # Permission is hereby granted, free of charge, to any person obtaining
+      # a copy of this software and associated documentation files (the
+      # "Software"), to deal in the Software without restriction, including
+      # without limitation the rights to use, copy, modify, merge, publish,
+      # distribute, sublicense, and/or sell copies of the Software, and to
+      # permit persons to whom the Software is furnished to do so, subject to
+      # the following conditions:
+      #
+      # The above copyright notice and this permission notice shall be
+      # included in all copies or substantial portions of the Software.
+      #
+      # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+      # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+      # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+      # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+      # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+      # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+      # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+      #
+      def processor_count!
+        os_name = RbConfig::CONFIG['target_os']
+        if os_name =~ /mingw|mswin/
+          require 'win32ole'
+          result = WIN32OLE.connect('winmgmts://').ExecQuery(
+              'select NumberOfLogicalProcessors from Win32_Processor')
+          result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
+        elsif File.readable?('/proc/cpuinfo')
+          IO.read('/proc/cpuinfo').scan(/^processor/).size
+        elsif File.executable?('/usr/bin/hwprefs')
+          IO.popen(%w[/usr/bin/hwprefs thread_count]).read.to_i
+        elsif File.executable?('/usr/sbin/psrinfo')
+          IO.popen('/usr/sbin/psrinfo').read.scan(/^.*on-*line/).size
+        elsif File.executable?('/usr/sbin/ioscan')
+          IO.popen(%w[/usr/sbin/ioscan -kC processor]) do |out|
+            out.read.scan(/^.*processor/).size
+          end
+        elsif File.executable?('/usr/sbin/pmcycles')
+          IO.popen(%w[/usr/sbin/pmcycles -m]).read.count("\n")
+        elsif File.executable?('/usr/sbin/lsdev')
+          IO.popen(%w[/usr/sbin/lsdev -Cc processor -S 1]).read.count("\n")
+        elsif File.executable?('/usr/sbin/sysconf') && os_name =~ /irix/i
+          IO.popen(%w[/usr/sbin/sysconf NPROC_ONLN]).read.to_i
+        elsif File.executable?('/usr/sbin/sysctl')
+          IO.popen(%w[/usr/sbin/sysctl -n hw.ncpu]).read.to_i
+        elsif File.executable?('/sbin/sysctl')
+          IO.popen(%w[/sbin/sysctl -n hw.ncpu]).read.to_i
+        else # unknown platform
+          1
+        end
+      rescue
+        1
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim.rb
new file mode 100644 (file)
index 0000000..a5ad079
--- /dev/null
@@ -0,0 +1,96 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  module VIM
+    autoload :Screen, 'command-t/vim/screen'
+    autoload :Window, 'command-t/vim/window'
+
+    class << self
+      # Check for the existence of a feature such as "conceal" or "syntax".
+      def has?(feature)
+        ::VIM::evaluate(%{has("#{feature}")}).to_i != 0
+      end
+
+      # Check for the presence of a setting such as:
+      #
+      #   - g:CommandTSmartCase (plug-in setting)
+      #   - &wildignore         (Vim setting)
+      #   - +cursorcolumn       (Vim setting, that works)
+      #
+      def exists?(str)
+        ::VIM::evaluate(%{exists("#{str}")}).to_i != 0
+      end
+
+      def get_number(name)
+        exists?(name) ? ::VIM::evaluate("#{name}").to_i : nil
+      end
+
+      def get_bool(name, default = nil)
+        exists?(name) ? ::VIM::evaluate("#{name}").to_i != 0 : default
+      end
+
+      def get_string(name)
+        exists?(name) ? ::VIM::evaluate("#{name}").to_s : nil
+      end
+
+      # expect a string or a list of strings
+      def get_list_or_string(name)
+        return nil unless exists?(name)
+        list_or_string = ::VIM::evaluate("#{name}")
+        if list_or_string.kind_of?(Array)
+          list_or_string.map { |item| item.to_s }
+        else
+          list_or_string.to_s
+        end
+      end
+
+      def pwd
+        ::VIM::evaluate 'getcwd()'
+      end
+
+      def current_file_dir
+        ::VIM::evaluate 'expand("%:p:h")'
+      end
+
+      # Execute cmd, capturing the output into a variable and returning it.
+      def capture(cmd)
+        ::VIM::command 'silent redir => g:command_t_captured_output'
+        ::VIM::command cmd
+        ::VIM::command 'silent redir END'
+        ::VIM::evaluate 'g:command_t_captured_output'
+      end
+
+      # Escape a string for safe inclusion in a Vim single-quoted string
+      # (single quotes escaped by doubling, everything else is literal)
+      def escape_for_single_quotes(str)
+        str.gsub "'", "''"
+      end
+
+      # Conservatively convert wildignore patterns that we understand to a
+      # regex. Supported patterns noted in the inline comments below.
+      #
+      # If this ends up doing something wrong, set `g:CommandTWildIgnore` to ''
+      # to opt out or otherwise override to produce a conforming pattern.
+      def wildignore_to_regexp(str)
+        patterns = str.split(',')
+        regex = patterns.map do |pattern|
+          if pattern.match(%r{\A([^*/]+)\z})
+            # something (match file at any level)
+            '(\A|/)' + Regexp.escape($~[1]) + '\z'
+          elsif pattern.match(%r{\A\*\.([^*]+)\z})
+            # *.something (match file with extension at any level)
+            '\.' + Regexp.escape($~[1]) + '\z'
+          elsif pattern.match(%r{\A\*/(.+)\z})
+            # */something (match files or directories at any level)
+            '(\A|/)' + Regexp.escape($~[1]) + '(/|\z)'
+          elsif pattern.match(%r{\A\*/([^*]+)/*\z})
+            # */something/* (match directories at any level)
+            '(\A|/)' + Regexp.escape($~[1]) + '(/|\z)'
+          end
+        end.compact.join('|')
+        Regexp.new(regex) unless regex.empty?
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim/screen.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim/screen.rb
new file mode 100644 (file)
index 0000000..59d801b
--- /dev/null
@@ -0,0 +1,14 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  module VIM
+    module Screen
+      class << self
+        def lines
+          ::VIM::evaluate('&lines').to_i
+        end
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim/window.rb b/tests/fixtures/integration/command-t/input/ruby/command-t/lib/command-t/vim/window.rb
new file mode 100644 (file)
index 0000000..966c1d1
--- /dev/null
@@ -0,0 +1,20 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+module CommandT
+  module VIM
+    module Window
+      class << self
+        def select(window)
+          return true if $curwin == window
+          initial = $curwin
+          while true do
+            ::VIM::command 'wincmd w'          # cycle through windows
+            return true if $curwin == window   # have selected desired window
+            return false if $curwin == initial # have already looped through all
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/controller_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/controller_spec.rb
new file mode 100644 (file)
index 0000000..c5266e9
--- /dev/null
@@ -0,0 +1,100 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Controller do
+  describe 'accept selection' do
+    let(:controller) { CommandT::Controller.new }
+
+    before do
+      check_ruby_1_9_2
+      stub_finder
+      stub_match_window 'path/to/selection'
+      stub_prompt
+      stub_vim '/working/directory'
+    end
+
+    def set_string(name, value)
+      stub(::VIM).evaluate(%{exists("#{name}")}).returns(1)
+      stub(::VIM).evaluate(name).returns(value)
+    end
+
+    it 'opens relative paths inside the working directory' do
+      stub(::VIM).evaluate('a:arg').returns('')
+      set_string('g:CommandTTraverseSCM', 'pwd')
+      controller.show_file_finder
+      mock(::VIM).command('silent e path/to/selection')
+      controller.accept_selection
+    end
+
+    it 'opens absolute paths outside the working directory' do
+      stub(::VIM).evaluate('a:arg').returns('../outside')
+      controller.show_file_finder
+      mock(::VIM).command('silent e /working/outside/path/to/selection')
+      controller.accept_selection
+    end
+
+    it 'does not get confused by common directory prefixes' do
+      stub(::VIM).evaluate('a:arg').returns('../directory-oops')
+      controller.show_file_finder
+      mock(::VIM).command('silent e /working/directory-oops/path/to/selection')
+      controller.accept_selection
+    end
+
+    it 'does not enter an infinite loop when toggling focus' do
+      # https://github.com/wincent/command-t/issues/157
+      stub(::VIM).evaluate('a:arg').returns('')
+      set_string('g:CommandTTraverseSCM', 'pwd')
+      controller.show_file_finder
+      expect { controller.toggle_focus }.to_not raise_error
+    end
+  end
+
+  def check_ruby_1_9_2
+    if RUBY_VERSION =~ /\A1\.9\.2/
+      pending 'broken in Ruby 1.9.2 (see https://gist.github.com/455547)'
+    end
+  end
+
+  def stub_finder(sorted_matches=[])
+    finder = CommandT::Finder::FileFinder.new
+    stub(finder).path = anything
+    stub(finder).sorted_matches_for(anything, anything).returns(sorted_matches)
+    stub(CommandT::Finder::FileFinder).new.returns(finder)
+  end
+
+  def stub_match_window(selection)
+    match_window = Object.new
+    stub(match_window).matches = anything
+    stub(match_window).leave
+    stub(match_window).focus
+    stub(match_window).selection.returns(selection)
+    stub(CommandT::MatchWindow).new.returns(match_window)
+  end
+
+  def stub_prompt(abbrev='')
+    prompt = Object.new
+    stub(prompt).focus
+    stub(prompt).unfocus
+    stub(prompt).clear!
+    stub(prompt).redraw
+    stub(prompt).abbrev.returns(abbrev)
+    stub(CommandT::Prompt).new.returns(prompt)
+  end
+
+  def stub_vim(working_directory)
+    stub($curbuf).number.returns('0')
+    stub(::VIM).command(/noremap/)
+    stub(::VIM).command('silent b 0')
+    stub(::VIM).command(/augroup/)
+    stub(::VIM).command('au!')
+    stub(::VIM).command(/autocmd/)
+    stub(::VIM).evaluate(/exists\(.+\)/).returns('0')
+    stub(::VIM).evaluate('getcwd()').returns(working_directory)
+    stub(::VIM).evaluate('&buflisted').returns('1')
+    stub(::VIM).evaluate('&lines').returns('80')
+    stub(::VIM).evaluate('&term').returns('vt100')
+    stub(::VIM).evaluate('v:version').returns(704)
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/finder/buffer_finder_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/finder/buffer_finder_spec.rb
new file mode 100644 (file)
index 0000000..f0df4e1
--- /dev/null
@@ -0,0 +1,54 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Finder::BufferFinder do
+  before do
+    @paths = %w(.git/config .vim/notes .vimrc baz foo/beta)
+    any_instance_of(CommandT::Scanner::BufferScanner, :paths => @paths)
+    @finder = CommandT::Finder::BufferFinder.new
+  end
+
+  describe 'sorted_matches_for method' do
+    it 'returns an empty array when no matches' do
+      expect(@finder.sorted_matches_for('kung foo fighting')).to eq([])
+    end
+
+    it 'returns all files when query string is empty' do
+      expect(@finder.sorted_matches_for('')).to eq(@paths)
+    end
+
+    it 'returns files in alphabetical order when query string is empty' do
+      results = @finder.sorted_matches_for('')
+      expect(results).to eq(results.sort)
+    end
+
+    it 'returns matching files in score order' do
+      expect(@finder.sorted_matches_for('ba')).to eq(%w(baz foo/beta))
+      expect(@finder.sorted_matches_for('a')).to eq(%w(baz foo/beta))
+    end
+
+    it 'returns matching dot files even when search term does not include a dot' do
+      expect(@finder.sorted_matches_for('i')).to include('.vimrc')
+    end
+
+    it 'returns matching files inside dot directories even when search term does not include a dot' do
+      expect(@finder.sorted_matches_for('i')).to include('.vim/notes')
+    end
+
+    it "does not consult the 'wildignore' setting" do
+      expect(@finder.sorted_matches_for('').count).to eq(5)
+    end
+
+    it 'obeys the :limit option for empty search strings' do
+      expect(@finder.sorted_matches_for('', :limit => 1)).
+        to eq(%w(.git/config))
+    end
+
+    it 'obeys the :limit option for non-empty search strings' do
+      expect(@finder.sorted_matches_for('i', :limit => 2)).
+        to eq(%w(.vimrc .vim/notes))
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/finder/file_finder_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/finder/file_finder_spec.rb
new file mode 100644 (file)
index 0000000..cac5388
--- /dev/null
@@ -0,0 +1,58 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Finder::FileFinder do
+  before :all do
+    @finder = CommandT::Finder::FileFinder.new File.join(File.dirname(__FILE__), '..',
+      '..', '..', 'fixtures')
+    @all_fixtures = %w(
+      bar/abc
+      bar/xyz
+      baz
+      bing
+      foo/alpha/t1
+      foo/alpha/t2
+      foo/beta
+    )
+  end
+
+  before do
+    stub(::VIM).evaluate(/expand/) { 0 }
+    stub(::VIM).command(/echon/)
+    stub(::VIM).command('redraw')
+  end
+
+  describe 'sorted_matches_for method' do
+    it 'returns an empty array when no matches' do
+      expect(@finder.sorted_matches_for('kung foo fighting')).to eq([])
+    end
+
+    it 'returns all files when query string is empty' do
+      expect(@finder.sorted_matches_for('')).to eq(@all_fixtures)
+    end
+
+    it 'returns files in alphabetical order when query string is empty' do
+      results = @finder.sorted_matches_for('')
+      expect(results).to eq(results.sort)
+    end
+
+    it 'returns matching files in score order' do
+      expect(@finder.sorted_matches_for('ba')).
+        to eq(%w(baz bar/abc bar/xyz foo/beta))
+      expect(@finder.sorted_matches_for('a')).
+        to eq(%w(baz bar/abc bar/xyz foo/alpha/t1 foo/alpha/t2 foo/beta))
+    end
+
+    it 'obeys the :limit option for empty search strings' do
+      expect(@finder.sorted_matches_for('', :limit => 2)).
+        to eq(%w(bar/abc bar/xyz))
+    end
+
+    it 'obeys the :limit option for non-empty search strings' do
+      expect(@finder.sorted_matches_for('a', :limit => 3)).
+        to eq(%w(baz bar/abc bar/xyz))
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/matcher_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/matcher_spec.rb
new file mode 100644 (file)
index 0000000..f76ddbf
--- /dev/null
@@ -0,0 +1,302 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+require 'ostruct'
+
+describe CommandT::Matcher do
+  def matcher(*paths)
+    scanner = OpenStruct.new(:paths => paths)
+    CommandT::Matcher.new(scanner)
+  end
+
+  describe 'initialization' do
+    it 'raises an ArgumentError if passed nil' do
+      expect { CommandT::Matcher.new(nil) }.to raise_error(ArgumentError)
+    end
+  end
+
+  describe '#sorted_matches_for' do
+    def ordered_matches(paths, query)
+      matcher(*paths).sorted_matches_for(query, :recurse => true)
+    end
+
+    it 'raises an ArgumentError if passed nil' do
+      expect { matcher.sorted_matches_for(nil) }.to raise_error(ArgumentError)
+    end
+
+    it 'returns empty array when source array empty' do
+      expect(matcher.sorted_matches_for('foo')).to eq([])
+      expect(matcher.sorted_matches_for('')).to eq([])
+    end
+
+    it 'returns empty array when no matches' do
+      matcher = matcher(*%w[foo/bar foo/baz bing])
+      expect(matcher.sorted_matches_for('xyz')).to eq([])
+    end
+
+    it 'returns matching paths' do
+      matcher = matcher(*%w[foo/bar foo/baz bing])
+      matches = matcher.sorted_matches_for('z')
+      expect(matches.map { |m| m.to_s }).to eq(['foo/baz'])
+      matches = matcher.sorted_matches_for('bg')
+      expect(matches.map { |m| m.to_s }).to eq(['bing'])
+    end
+
+    it 'performs case-insensitive matching' do
+      matches = matcher('Foo').sorted_matches_for('f')
+      expect(matches.map { |m| m.to_s }).to eq(['Foo'])
+    end
+
+    it 'considers the space character to match a literal space' do
+      paths = ['path_no_space', 'path with/space']
+      matches = matcher(*paths).sorted_matches_for('path space')
+      expect(matches.map { |m| m.to_s }).to eq(['path with/space'])
+    end
+
+    context 'when the ignore_spaces option in specified' do
+      it 'ignores the space character' do
+        paths = ['path_no_space', 'path with/space']
+        matches = matcher(*paths).sorted_matches_for('path space', :ignore_spaces => true)
+        expect(matches.map { |m| m.to_s }).to eq(['path_no_space', 'path with/space'])
+      end
+    end
+
+    it 'considers the empty string to match everything' do
+      matches = matcher('foo').sorted_matches_for('')
+      expect(matches.map { |m| m.to_s }).to eq(['foo'])
+    end
+
+    # Can't imagine this happening in practice, but want to handle it in case.
+    it 'gracefully handles empty haystacks' do
+      expect(matcher('', 'foo').sorted_matches_for('').map { |m| m.to_s }).to eq(['', 'foo'])
+      expect(matcher('', 'foo').sorted_matches_for('f').map { |m| m.to_s }).to eq(['foo'])
+    end
+
+    it 'does not consider mere substrings of the query string to be a match' do
+      expect(matcher('foo').sorted_matches_for('foo...')).to eq([])
+    end
+
+    it 'prioritizes shorter paths over longer ones' do
+      expect(ordered_matches(%w[
+        articles_controller_spec.rb
+        article.rb
+      ], 'art')).to eq(%w[
+        article.rb
+        articles_controller_spec.rb
+      ])
+    end
+
+    it 'prioritizes matches after "/"' do
+      expect(ordered_matches(%w[fooobar foo/bar], 'b')).to eq(%w[foo/bar fooobar])
+
+      # note that / beats _
+      expect(ordered_matches(%w[foo_bar foo/bar], 'b')).to eq(%w[foo/bar foo_bar])
+
+      # / also beats -
+      expect(ordered_matches(%w[foo-bar foo/bar], 'b')).to eq(%w[foo/bar foo-bar])
+
+      # and numbers
+      expect(ordered_matches(%w[foo9bar foo/bar], 'b')).to eq(%w[foo/bar foo9bar])
+
+      # and periods
+      expect(ordered_matches(%w[foo.bar foo/bar], 'b')).to eq(%w[foo/bar foo.bar])
+
+      # and spaces
+      expect(ordered_matches(['foo bar', 'foo/bar'], 'b')).to eq(['foo/bar', 'foo bar'])
+    end
+
+    it 'prioritizes matches after "-"' do
+      expect(ordered_matches(%w[fooobar foo-bar], 'b')).to eq(%w[foo-bar fooobar])
+
+      # - also beats .
+      expect(ordered_matches(%w[foo.bar foo-bar], 'b')).to eq(%w[foo-bar foo.bar])
+    end
+
+    it 'prioritizes matches after "_"' do
+      expect(ordered_matches(%w[fooobar foo_bar], 'b')).to eq(%w[foo_bar fooobar])
+
+      # _ also beats .
+      expect(ordered_matches(%w[foo.bar foo_bar], 'b')).to eq(%w[foo_bar foo.bar])
+    end
+
+    it 'prioritizes matches after " "' do
+      expect(ordered_matches(['fooobar', 'foo bar'], 'b')).to eq(['foo bar', 'fooobar'])
+
+      # " " also beats .
+      expect(ordered_matches(['foo.bar', 'foo bar'], 'b')).to eq(['foo bar', 'foo.bar'])
+    end
+
+    it 'prioritizes matches after numbers' do
+      expect(ordered_matches(%w[fooobar foo9bar], 'b')).to eq(%w[foo9bar fooobar])
+
+      # numbers also beat .
+      expect(ordered_matches(%w[foo.bar foo9bar], 'b')).to eq(%w[foo9bar foo.bar])
+    end
+
+    it 'prioritizes matches after periods' do
+      expect(ordered_matches(%w[fooobar foo.bar], 'b')).to eq(%w[foo.bar fooobar])
+    end
+
+    it 'prioritizes matching capitals following lowercase' do
+      expect(ordered_matches(%w[foobar fooBar], 'b')).to eq(%w[fooBar foobar])
+    end
+
+    it 'prioritizes matches earlier in the string' do
+      expect(ordered_matches(%w[******b* **b*****], 'b')).to eq(%w[**b***** ******b*])
+    end
+
+    it 'prioritizes matches closer to previous matches' do
+      expect(ordered_matches(%w[**b***c* **bc****], 'bc')).to eq(%w[**bc**** **b***c*])
+    end
+
+    it 'scores alternative matches of same path differently' do
+      # ie:
+      # app/controllers/articles_controller.rb
+      expect(ordered_matches(%w[
+        a**/****r******/**t*c***_*on*******.**
+        ***/***********/art*****_con*******.**
+      ], 'artcon')).to eq(%w[
+        ***/***********/art*****_con*******.**
+        a**/****r******/**t*c***_*on*******.**
+      ])
+    end
+
+    it 'provides intuitive results for "artcon" and "articles_controller"' do
+      expect(ordered_matches(%w[
+        app/controllers/heartbeat_controller.rb
+        app/controllers/articles_controller.rb
+      ], 'artcon')).to eq(%w[
+        app/controllers/articles_controller.rb
+        app/controllers/heartbeat_controller.rb
+      ])
+    end
+
+    it 'provides intuitive results for "aca" and "a/c/articles_controller"' do
+      expect(ordered_matches(%w[
+        app/controllers/heartbeat_controller.rb
+        app/controllers/articles_controller.rb
+      ], 'aca')).to eq(%w[
+        app/controllers/articles_controller.rb
+        app/controllers/heartbeat_controller.rb
+      ])
+    end
+
+    it 'provides intuitive results for "d" and "doc/command-t.txt"' do
+      expect(ordered_matches(%w[
+        TODO
+        doc/command-t.txt
+      ], 'd')).to eq(%w[
+        doc/command-t.txt
+        TODO
+      ])
+    end
+
+    it 'provides intuitive results for "do" and "doc/command-t.txt"' do
+      expect(ordered_matches(%w[
+        TODO
+        doc/command-t.txt
+      ], 'do')).to eq(%w[
+        doc/command-t.txt
+        TODO
+      ])
+    end
+
+    it 'provides intuitive results for "matchh" search' do
+      # Regression introduced in 187bc18.
+      expect(ordered_matches(%w[
+        vendor/bundle/ruby/1.8/gems/rspec-expectations-2.14.5/spec/rspec/matchers/has_spec.rb
+        ruby/command-t/match.h
+      ], 'matchh')).to eq(%w[
+        ruby/command-t/match.h
+        vendor/bundle/ruby/1.8/gems/rspec-expectations-2.14.5/spec/rspec/matchers/has_spec.rb
+      ])
+    end
+
+    it 'provides intuitive results for "relqpath" search' do
+      # Another regression.
+      expect(ordered_matches(%w[
+        *l**/e*t*t*/atla*/patter**/E*tAtla***el****q*e*e***al**at***HelperTra*t.php
+        static_upstream/relay/query/RelayQueryPath.js
+      ], 'relqpath')).to eq(%w[
+        static_upstream/relay/query/RelayQueryPath.js
+        *l**/e*t*t*/atla*/patter**/E*tAtla***el****q*e*e***al**at***HelperTra*t.php
+      ])
+    end
+
+    it 'provides intuitive results for "controller" search' do
+      # Another regression.
+      expect(ordered_matches(%w[
+        spec/command-t/controller_spec.rb
+        ruby/command-t/controller.rb
+      ], 'controller')).to eq(%w[
+        ruby/command-t/controller.rb
+        spec/command-t/controller_spec.rb
+      ])
+    end
+
+    it "doesn't incorrectly accept repeats of the last-matched character" do
+      # https://github.com/wincent/Command-T/issues/82
+      matcher = matcher(*%w[ash/system/user/config.h])
+      expect(matcher.sorted_matches_for('usercc')).to eq([])
+
+      # simpler test case
+      matcher = matcher(*%w[foobar])
+      expect(matcher.sorted_matches_for('fooooo')).to eq([])
+
+      # minimal repro
+      matcher = matcher(*%w[ab])
+      expect(matcher.sorted_matches_for('aa')).to eq([])
+    end
+
+    it 'ignores dotfiles by default' do
+      matcher = matcher(*%w[.foo .bar])
+      expect(matcher.sorted_matches_for('foo')).to eq([])
+    end
+
+    it 'shows dotfiles if the query starts with a dot' do
+      matcher = matcher(*%w[.foo .bar])
+      expect(matcher.sorted_matches_for('.fo')).to eq(%w[.foo])
+    end
+
+    it "doesn't show dotfiles if the query contains a non-leading dot" do
+      matcher = matcher(*%w[.foo.txt .bar.txt])
+      expect(matcher.sorted_matches_for('f.t')).to eq([])
+
+      # counter-example
+      expect(matcher.sorted_matches_for('.f.t')).to eq(%w[.foo.txt])
+    end
+
+    it "shows dotfiles when there is a non-leading dot that matches a leading dot within a path component" do
+      matcher = matcher(*%w[this/.secret/stuff.txt something.else])
+      expect(matcher.sorted_matches_for('t.sst')).to eq(%w[this/.secret/stuff.txt])
+    end
+
+    it "doesn't show a dotfile just because there was a match at index 0" do
+      pending 'fix'
+      matcher = matcher(*%w[src/.flowconfig])
+      expect(matcher.sorted_matches_for('s')).to eq([])
+    end
+
+    it 'correctly computes non-recursive match score' do
+      # Non-recursive match was incorrectly inflating some scores.
+      # Related: https://github.com/wincent/command-t/issues/209
+      matcher = matcher(*%w[
+        app/assets/components/App/index.jsx
+        app/assets/components/PrivacyPage/index.jsx
+        app/views/api/docs/pagination/_index.md
+      ])
+
+      # You might want the second match here to come first, but in the
+      # non-recursive case we greedily match the "app" in "app", the "a" in
+      # "assets", the "p" in "components", and the first "p" in "App". This
+      # doesn't score as favorably as matching the "app" in "app", the "ap" in
+      # "api", and the "p" in "pagination".
+      expect(matcher.sorted_matches_for('appappind')).to eq(%w[
+        app/views/api/docs/pagination/_index.md
+        app/assets/components/App/index.jsx
+        app/assets/components/PrivacyPage/index.jsx
+      ])
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/scanner/buffer_scanner_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/scanner/buffer_scanner_spec.rb
new file mode 100644 (file)
index 0000000..acc3b7e
--- /dev/null
@@ -0,0 +1,29 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+require 'ostruct'
+
+describe CommandT::Scanner::BufferScanner do
+  def buffer(name)
+    b = OpenStruct.new
+    b.name = name
+    b
+  end
+
+  before do
+    @paths = %w(bar/abc bar/xyz baz bing foo/alpha/t1 foo/alpha/t2 foo/beta)
+    @scanner = CommandT::Scanner::BufferScanner.new
+    stub(@scanner).relative_path_under_working_directory(is_a(String)) { |arg| arg }
+    stub(::VIM::Buffer).count { 7 }
+    (0..6).each do |n|
+      stub(::VIM::Buffer)[n].returns(buffer @paths[n])
+    end
+  end
+
+  describe 'paths method' do
+    it 'returns a list of regular files' do
+      expect(@scanner.paths).to match_array(@paths)
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner/ruby_file_scanner_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner/ruby_file_scanner_spec.rb
new file mode 100644 (file)
index 0000000..10f0e22
--- /dev/null
@@ -0,0 +1,64 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Scanner::FileScanner::RubyFileScanner do
+  before do
+    @dir = File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'fixtures')
+    @all_fixtures = %w(
+      bar/abc bar/xyz baz bing foo/alpha/t1 foo/alpha/t2 foo/beta
+    )
+    @scanner = CommandT::Scanner::FileScanner::RubyFileScanner.new(@dir)
+
+    stub(::VIM).evaluate(/exists/) { 1 }
+    stub(::VIM).evaluate(/expand\(.+\)/) { '0' }
+    stub(::VIM).command(/echon/)
+    stub(::VIM).command('redraw')
+  end
+
+  describe 'paths method' do
+    it 'returns a list of regular files' do
+      expect(@scanner.paths).to match_array(@all_fixtures)
+    end
+  end
+
+  describe 'path= method' do
+    it 'allows repeated applications of scanner at different paths' do
+      expect(@scanner.paths).to match_array(@all_fixtures)
+
+      # drill down 1 level
+      @scanner.path = File.join(@dir, 'foo')
+      expect(@scanner.paths).to match_array(%w(alpha/t1 alpha/t2 beta))
+
+      # and another
+      @scanner.path = File.join(@dir, 'foo', 'alpha')
+      expect(@scanner.paths).to match_array(%w(t1 t2))
+    end
+  end
+
+  describe "'wildignore' exclusion" do
+    context "when there is a 'wildignore' setting in effect" do
+      it "filters out matching files" do
+        scanner =
+          CommandT::Scanner::FileScanner::RubyFileScanner.new @dir,
+            :wildignore => CommandT::VIM::wildignore_to_regexp('xyz')
+        expect(scanner.paths.count).to eq(@all_fixtures.count - 1)
+      end
+    end
+
+    context "when there is no 'wildignore' setting in effect" do
+      it "does nothing" do
+        scanner = CommandT::Scanner::FileScanner::RubyFileScanner.new @dir
+        expect(scanner.paths.count).to eq(@all_fixtures.count)
+      end
+    end
+  end
+
+  describe ':max_depth option' do
+    it 'does not descend below "max_depth" levels' do
+      @scanner = CommandT::Scanner::FileScanner::RubyFileScanner.new @dir, :max_depth => 1
+      expect(@scanner.paths).to match_array(%w(bar/abc bar/xyz baz bing foo/beta))
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner/watchman_file_scanner_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner/watchman_file_scanner_spec.rb
new file mode 100644 (file)
index 0000000..f6ec70d
--- /dev/null
@@ -0,0 +1,23 @@
+# Copyright 2015-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Scanner::FileScanner::WatchmanFileScanner do
+  context 'when an error occurs' do
+    it 'falls back to the FindFileScanner' do
+      # fake an error
+      scanner = described_class.new
+      stub(scanner).get_raw_sockname do
+        raise described_class::WatchmanError
+      end
+
+      # expect call on superclass
+      any_instance_of(CommandT::Scanner::FileScanner::FindFileScanner) do |klass|
+        mock(klass).paths!
+      end
+
+      scanner.paths!
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/scanner/file_scanner_spec.rb
new file mode 100644 (file)
index 0000000..d38cc2f
--- /dev/null
@@ -0,0 +1,18 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Scanner::FileScanner do
+  before do
+    dir = File.join(File.dirname(__FILE__), '..', '..', '..', 'fixtures')
+    @scanner = CommandT::Scanner::FileScanner.new(dir)
+  end
+
+  describe 'flush method' do
+    it 'forces a rescan on next call to paths method' do
+      expect { @scanner.flush }.
+        to change { @scanner.instance_variable_get('@paths').object_id }
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/vim_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/vim_spec.rb
new file mode 100644 (file)
index 0000000..ce106ab
--- /dev/null
@@ -0,0 +1,14 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::VIM do
+  describe '.escape_for_single_quotes' do
+    it 'turns doubles all single quotes' do
+      input = %{it's ''something''}
+      expected = %{it''s ''''something''''}
+      expect(CommandT::VIM.escape_for_single_quotes(input)).to eq(expected)
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/command-t/watchman/utils_spec.rb b/tests/fixtures/integration/command-t/input/spec/command-t/watchman/utils_spec.rb
new file mode 100644 (file)
index 0000000..94fd3eb
--- /dev/null
@@ -0,0 +1,415 @@
+# Copyright 2014-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+require 'spec_helper'
+
+describe CommandT::Watchman::Utils do
+  def binary(str)
+    if str.respond_to?(:force_encoding) # Ruby >= 1.9
+      str.force_encoding('ASCII-8BIT')
+    else
+      str
+    end
+  end
+
+  def little_endian?
+    byte = [0xff00].pack('s')[0]
+    if byte.is_a?(Fixnum) # ie. Ruby 1.8
+      byte.zero?
+    elsif byte.is_a?(String) # ie. Ruby >= 1.9
+      byte == "\x00"
+    else
+      raise 'unable to determine endianness'
+    end
+  end
+
+  def roundtrip(value)
+    described_class.load(described_class.dump(value))
+  end
+
+  it 'roundtrips arrays' do
+    value = [1, 2, ['three', false]]
+    expect(roundtrip(value)).to eq(value)
+  end
+
+  it 'roundtrips hashes' do
+    value = {
+      'foo' => 1,
+      'bar' => {
+        'baz' => 'bing',
+      }
+    }
+    expect(roundtrip(value)).to eq(value)
+  end
+
+  it 'roundtrips strings' do
+    expect(roundtrip('')).to eq('')
+    expect(roundtrip('/foo/bar/baz')).to eq('/foo/bar/baz')
+  end
+
+  it 'roundtrips uint8_t integers' do
+    expect(roundtrip(0)).to eq(0)
+    expect(roundtrip(1)).to eq(1)
+    expect(roundtrip(0xff)).to eq(0xff)
+  end
+
+  it 'roundtrips uint16_t integers' do
+    expect(roundtrip(0x1234)).to eq(0x1234)
+  end
+
+  it 'roundtrips uint32_t integers' do
+    expect(roundtrip(0x12345678)).to eq(0x12345678)
+  end
+
+  it 'roundtrips uint64_t integers' do
+    expect(roundtrip(0x12345678abcdef00)).to eq(0x12345678abcdef00)
+  end
+
+  it 'roundtrips floats' do
+    expect(roundtrip(1234.5678)).to eq(1234.5678)
+  end
+
+  it 'roundtrips `true` booleans' do
+    expect(roundtrip(true)).to be true
+  end
+
+  it 'roundtrips `false` booleans' do
+    expect(roundtrip(false)).to be false
+  end
+
+  it 'roundtrips nil' do
+    expect(roundtrip(nil)).to be nil
+  end
+
+  describe '.load' do
+    it 'rejects undersized input' do
+      expect { described_class.load('') }.
+        to raise_error(ArgumentError, /undersized/i)
+    end
+
+    it 'rejects input without a binary marker' do
+      expect { described_class.load('gibberish') }.
+        to raise_error(ArgumentError, /missing/i)
+    end
+
+    it 'rejects a missing payload header' do
+      # binary protocol marker, but nothing else
+      input = binary("\x00\x01")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /undersized/i)
+    end
+
+    it 'rejects empty payloads' do
+      # uint8_t size marker of zero
+      input = binary("\x00\x01\x03\x00")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /empty/i)
+    end
+
+    it 'rejects unrecognized payload markers' do
+      # 0x10 is not a valid integer marker
+      input = binary("\x00\x01\x10\x00")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /bad integer/i)
+    end
+
+    it 'rejects undersized payload markers' do
+      # int16_t marker, but only storage for int8_t
+      input = binary("\x00\x01\x04\x00")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /overrun\b.+\bint16_t/i)
+    end
+
+    it 'loads array values' do
+      input = binary(
+        "\x00\x01\x03\x16\x00\x03\x05\x03\x01\x02\x03" \
+        "\x06foobar\x08\x09\x00\x03\x02\x03\x0a\x0a"
+      )
+      expect(described_class.load(input)).
+        to eq([1, 'foobar', true, false, [10, nil]])
+    end
+
+    it 'handles empty arrays' do
+      input = binary("\x00\x01\x03\x03\x00\x03\x00")
+      expect(described_class.load(input)).to eq([])
+    end
+
+    it 'rejects arrays with incomplete headers' do
+      input = binary("\x00\x01\x03\x02\x00\x03")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /incomplete array header/i)
+    end
+
+    it 'rejects arrays with incomplete entries' do
+      input = binary("\x00\x01\x03\x05\x00\x03\x10\x0a\x0a")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /unexpected end/i)
+    end
+
+    it 'loads hash values' do
+      input = binary(
+        "\x00\x01\x03\x1a\x01\x03\x02\x02\x03\x03foo\x0a" \
+        "\x02\x03\x03bar\x01\x03\x01\x02\x03\x03baz\x08"
+      )
+      expected = {
+        'foo' => nil,
+        'bar' => {
+          'baz' => true,
+        }
+      }
+      expect(described_class.load(input)).to eq(expected)
+    end
+
+    it 'handles empty hashes' do
+      input = binary("\x00\x01\x03\x03\x01\x03\x00")
+      expect(described_class.load(input)).to eq({})
+    end
+
+    it 'rejects hashes with incomplete headers' do
+      input = binary("\x00\x01\x03\x02\x01\x03")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /incomplete hash header/i)
+    end
+
+    it 'rejects hashes with invalid keys' do
+      # keys must be strings; this one uses uses a number instead
+      input = binary("\x00\x01\x03\x05\x01\x03\x01\x03\x00")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /not a number/i)
+    end
+
+    it 'rejects hashes with missing keys' do
+      input = binary("\x00\x01\x03\x03\x01\x03\x01")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /unexpected end/i)
+    end
+
+    it 'rejects hashes with missing values' do
+      input = binary("\x00\x01\x03\x09\x01\x03\x01\x02\x03\x03foo")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /unexpected end/i)
+    end
+
+    it 'loads string values' do
+      input = binary("\x00\x01\x03\x06\x02\x03\x03foo")
+      expect(described_class.load(input)).to eq('foo')
+    end
+
+    it 'handles empty strings' do
+      input = binary("\x00\x01\x03\x03\x02\x03\x00")
+      expect(described_class.load(input)).to eq('')
+    end
+
+    if String.new.respond_to?(:encoding) # ie. Ruby >= 1.9
+      it 'loads string values as ASCII-8BIT encoded strings' do
+        input = binary("\x00\x01\x03\x06\x02\x03\x03foo")
+        expect(described_class.load(input).encoding.to_s).to eq('ASCII-8BIT')
+      end
+    end
+
+    it 'rejects string values with incomplete headers' do
+      input = binary("\x00\x01\x03\x01\x02")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /invalid string header/i)
+    end
+
+    it 'rejects string values with invalid headers' do
+      # expect a number indicating the string length, get a boolean instead
+      input = binary("\x00\x01\x03\x05\x02\x08foo")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /bad integer/i)
+    end
+
+    it 'rejects string values with insufficient storage' do
+      # expect 3 bytes, get 2 instead
+      input = binary("\x00\x01\x03\x05\x02\x03\x03fo")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /insufficient string storage/i)
+    end
+
+    it 'loads uint8_t values' do
+      input = binary("\x00\x01\x03\x02\x03\x12")
+      expect(described_class.load(input)).to eq(0x12)
+    end
+
+    it 'loads uint16_t values' do
+      if little_endian?
+        input = binary("\x00\x01\x03\x03\x04\x34\x12")
+      else
+        input = binary("\x00\x01\x03\x03\x04\x12\x34")
+      end
+
+      expect(described_class.load(input)).to eq(0x1234)
+    end
+
+    it 'loads uint32_t values' do
+      if little_endian?
+        input = binary("\x00\x01\x03\x05\x05\x78\x56\x34\x12")
+      else
+        input = binary("\x00\x01\x03\x05\x05\x12\x34\x56\x78")
+      end
+
+      expect(described_class.load(input)).to eq(0x12345678)
+    end
+
+    it 'loads int uint64_t values' do
+      if little_endian?
+        input = binary("\x00\x01\x03\x09\x06\xef\xcd\xab\x90\x78\x56\x34\x12")
+      else
+        input = binary("\x00\x01\x03\x09\x06\x12\x34\x56\x78\x90\xab\xcd\xef")
+      end
+      expect(described_class.load(input)).to eq(0x1234567890abcdef)
+    end
+
+    it 'rejects int markers with missing values' do
+      # expect an integer, but hit the end of the buffer
+      input = binary("\x00\x01\x03\x01\x05")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /insufficient int storage/i)
+    end
+
+    it 'rejects double markers with insufficient storage' do
+      # double with 7 bytes of storage instead of the required 8 bytes
+      input = binary("\x00\x01\x03\x08\x07\x00\x00\x00\x00\x00\x00\x00")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /insufficient double storage/i)
+    end
+
+    it 'loads boolean `true` values' do
+      input = binary("\x00\x01\x03\x01\x08")
+      expect(described_class.load(input)).to be true
+    end
+
+    it 'loads boolean `false` values' do
+      input = binary("\x00\x01\x03\x01\x09")
+      expect(described_class.load(input)).to be false
+    end
+
+    it 'loads nil' do
+      input = binary("\x00\x01\x03\x01\x0a")
+      expect(described_class.load(input)).to be nil
+    end
+
+    it 'loads templates' do
+      # this example includes a "skip" marker
+      input = binary(
+        "\x00\x01\x03\x28\x0b\x00\x03\x02\x02\x03\x04name" \
+        "\x02\x03\x03age\x03\x03\x02\x03\x04fred\x03" \
+        "\x14\x02\x03\x04pete\x03\x1e\x0c\x03\x19"
+      )
+      expected = [
+        { 'name' => 'fred', 'age' => 20 },
+        { 'name' => 'pete', 'age' => 30 },
+        { 'age' => 25  },
+      ]
+      expect(described_class.load(input)).to eq(expected)
+    end
+
+    it 'handles empty templates' do
+      input = binary(
+        "\x00\x01\x03\x12\x0b\x00\x03\x02\x02" \
+        "\x03\x03foo\x02\x03\x03bar\x03\x00"
+      )
+      expect(described_class.load(input)).to eq([])
+    end
+
+    it 'rejects templates without a header array' do
+      input = binary("\x00\x01\x03\x01\x0b")
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /unexpected end/i)
+    end
+
+    it 'rejects templates without a row items array' do
+      input = binary(
+        "\x00\x01\x03\x10\x0b\x00\x03\x02\x02" \
+        "\x03\x03foo\x02\x03\x03bar"
+      )
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /insufficient/i)
+    end
+
+    it 'rejects templates with non-string header items' do
+      input = binary(
+        "\x00\x01\x03\x0e\x0b\x00\x03\x02\x02" \
+        "\x03\x03foo\x03\x03\x03\x00"
+      )
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /not a number/)
+    end
+
+    it 'rejects templates with a header item array count mismatch' do
+      input = binary(
+        "\x00\x01\x03\x0a\x0b\x00\x03\x02\x02" \
+        "\x03\x03foo"
+      )
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /unexpected end/)
+    end
+
+    it 'rejects templates with a row item count mismatch' do
+      input = binary(
+        "\x00\x01\x03\x25\x0b\x00\x03\x02\x02\x03\x04name" \
+        "\x02\x03\x03age\x03\x03\x02\x03\x04fred\x03" \
+        "\x14\x02\x03\x04pete\x03\x1e"
+      )
+      expect { described_class.load(input) }.
+        to raise_error(ArgumentError, /unexpected end/)
+    end
+  end
+
+  describe '.dump' do
+    let(:query) do
+      # this is the typical kind of query that Command-T will actually issue
+      ['query', '/some/path', {
+        'expression' => ['type', 'f'],
+        'fields'     => ['name'],
+      }]
+    end
+
+    it 'serializes' do
+      expect { described_class.dump(query) }.to_not raise_error
+    end
+
+    if String.new.respond_to?(:encoding) # ie. Ruby >= 1.9
+      it 'serializes to an ASCII-8BIT string' do
+        expect(described_class.dump(query).encoding.to_s).to eq('ASCII-8BIT')
+      end
+    end
+
+    it 'generates a correct serialization' do
+      # in Ruby 1.8, hashes aren't ordered, so two serializations are possible
+      if little_endian?
+        expected = [
+          binary(
+            "\x00\x01\x06\x49\x00\x00\x00\x00\x00\x00\x00\x00\x03\x03\x02\x03" \
+            "\x05query\x02\x03\x0a/some/path\x01\x03\x02\x02\x03\x0a" \
+            "expression\x00\x03\x02\x02\x03\x04type\x02\x03\x01f\x02\x03\x06" \
+            "fields\x00\x03\x01\x02\x03\x04name"
+          ),
+          binary(
+            "\x00\x01\x06\x49\x00\x00\x00\x00\x00\x00\x00\x00\x03\x03\x02\x03" \
+            "\x05query\x02\x03\x0a/some/path\x01\x03\x02\x02\x03\x06fields" \
+            "\x00\x03\x01\x02\x03\x04name\x02\x03\x0aexpression\x00\x03\x02" \
+            "\x02\x03\x04type\x02\x03\x01f"
+          )
+        ]
+      else
+        expected = [
+          binary(
+            "\x00\x01\x06\x00\x00\x00\x00\x00\x00\x00\x49\x00\x03\x03\x02\x03" \
+            "\x05query\x02\x03\x0a/some/path\x01\x03\x02\x02\x03\x0a" \
+            "expression\x00\x03\x02\x02\x03\x04type\x02\x03\x01f\x02\x03\x06" \
+            "fields\x00\x03\x01\x02\x03\x04name"
+          ),
+          binary(
+            "\x00\x01\x06\x00\x00\x00\x00\x00\x00\x00\x49\x00\x03\x03\x02\x03" \
+            "\x05query\x02\x03\x0a/some/path\x01\x03\x02\x02\x03\x06fields" \
+            "\x00\x03\x01\x02\x03\x04name\x02\x03\x0aexpression\x00\x03\x02" \
+            "\x02\x03\x04type\x02\x03\x01f"
+          )
+        ]
+      end
+      expect(expected).to include(described_class.dump(query))
+    end
+  end
+end
diff --git a/tests/fixtures/integration/command-t/input/spec/spec_helper.rb b/tests/fixtures/integration/command-t/input/spec/spec_helper.rb
new file mode 100644 (file)
index 0000000..2775956
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright 2010-present Greg Hurrell. All rights reserved.
+# Licensed under the terms of the BSD 2-clause license.
+
+if !Object.const_defined?('Bundler')
+  require 'rubygems'
+  require 'bundler'
+  Bundler.setup
+end
+require 'rspec'
+
+ext = File.expand_path('../ruby/command-t/lib', File.dirname(__FILE__))
+lib = File.expand_path('../ruby/command-t/ext', File.dirname(__FILE__))
+$LOAD_PATH.unshift(ext) unless $LOAD_PATH.include?(ext)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+
+require 'command-t'
+require 'command-t/ext'
+
+RSpec.configure do |config|
+  config.mock_framework = :rr
+end
+
+# Fake top-level VIM implementation, for stubbing.
+module VIM
+  class << self
+    def evaluate(*args); end
+  end
+
+  Buffer = Class.new
+end