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 #"
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"
;;
*)
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 #"
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"
;;
*)
--- /dev/null
+"(eval)" (line 134, column 38):
+unexpected "_"
+expecting letter or digit, "\n", "," or ")"
--- /dev/null
+"(eval)" (line 134, column 38):
+unexpected "_"
+expecting letter or digit, "\n", "," or ")"
--- /dev/null
+"(eval)" (line 134, column 38):
+unexpected "_"
+expecting letter or digit, "\n", "," or ")"
--- /dev/null
+--exclude=.git
+--exclude=Makefile
+--exclude=vendor
--- /dev/null
+data/benchmark.yml export-ignore
--- /dev/null
+.release-notes.txt
+.ruby-version
+/.bundle
+/data/log.yml
+vendor/bundle
--- /dev/null
+[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
--- /dev/null
+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>
--- /dev/null
+source 'https://rubygems.org'
+
+gem 'nokogiri'
+gem 'mechanize'
+gem 'rake'
+gem 'rr'
+gem 'rspec'
--- /dev/null
+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
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+<?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>
--- /dev/null
+" 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
--- /dev/null
+" 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
--- /dev/null
+" 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
--- /dev/null
+" 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
--- /dev/null
+#!/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)
--- /dev/null
+#!/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
--- /dev/null
+" 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
--- /dev/null
+#!/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
--- /dev/null
+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
--- /dev/null
+/ext.*
+!/ext.c
+!/ext.h
+/*.log
+/*.o
+/Makefile
+/metadata.rb
--- /dev/null
+# 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
--- /dev/null
+// 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);
+}
--- /dev/null
+// 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)
--- /dev/null
+# 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
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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);
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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
+);
--- /dev/null
+// 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;
+}
--- /dev/null
+// 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);
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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);
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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