dotar/vim/plugin/lusty-explorer.vim
2011-11-17 16:00:49 -06:00

2378 lines
66 KiB
VimL
Executable File

" Copyright: Copyright (C) 2007-2011 Stephen Bach
" Permission is hereby granted to use and distribute this code,
" with or without modifications, provided that this copyright
" notice is copied with it. Like anything else that's free,
" lusty-explorer.vim is provided *as is* and comes with no
" warranty of any kind, either expressed or implied. In no
" event will the copyright holder be liable for any damages
" resulting from the use of this software.
"
" Name Of File: lusty-explorer.vim
" Description: Dynamic Filesystem and Buffer Explorer Vim Plugin
" Maintainers: Stephen Bach <this-file@sjbach.com>
" Matt Tolton <matt-lusty-explorer@tolton.com>
" Contributors: Raimon Grau, Sergey Popov, Yuichi Tateno, Bernhard Walle,
" Rajendra Badapanda, cho45, Simo Salminen, Sami Samhuri,
" Matt Tolton, Björn Winckler, sowill, David Brown
" Brett DiFrischia, Ali Asad Lotia, Kenneth Love, Ben Boeckel,
" robquant, lilydjwg, Martin Wache, Johannes Holzfuß
" Donald Curtis, Jan Zwiener, Giuseppe Rota, Toby O'Connell
"
" Release Date: April 29, 2011
" Version: 4.1
"
" Usage:
" <Leader>lf - Opens the filesystem explorer.
" <Leader>lr - Opens the filesystem explorer from the
" directory of the current file.
" <Leader>lb - Opens the buffer explorer.
" <Leader>lg - Opens the buffer grep, for searching through
" all loaded buffers
"
" You can also use the commands:
"
" ":LustyFilesystemExplorer [optional-path]"
" ":LustyFilesystemExplorerFromHere"
" ":LustyBufferExplorer"
" ":LustyBufferGrep"
"
" (Personally, I map these to ,f ,r ,b and ,g)
"
" When launched, a new window appears at bottom presenting a
" table of files/dirs or buffers, and in the status bar a
" prompt:
"
" >>
"
" As you type, the table updates for possible matches using a
" fuzzy matching algorithm (or regex matching, in the case of
" grep). Special keys include:
"
" <Enter> open selected match
" <Tab> open selected match
" <Esc> cancel
" <C-c> cancel
" <C-g> cancel
"
" <C-t> open selected match in a new [t]ab
" <C-o> open selected match in a new h[o]rizontal split
" <C-v> open selected match in a new [v]ertical split
"
" <C-n> select [n]ext match
" <C-p> select [p]revious match
" <C-f> select [f]orward one column
" <C-b> select [b]ack one column
"
" <C-u> clear prompt
"
" Additional shortcuts for the filesystem explorer:
"
" <C-w> ascend one directory at prompt
" <C-r> [r]efresh directory contents
" <C-a> open [a]ll files in current table
" <C-e> create new buffer with the given name and path
"
" Filesystem Explorer:
"
" - Directory contents are memoized. (<C-r> to refresh.)
" - You can recurse into and out of directories by typing the directory name
" and a slash, e.g. "stuff/" or "../".
" - Variable expansion, e.g. "$D" -> "/long/dir/path/".
" - Tilde (~) expansion, e.g. "~/" -> "/home/steve/".
" - Dotfiles are hidden by default, but are shown if the current search term
" begins with a '.'. To show these file at all times, set this option:
"
" let g:LustyExplorerAlwaysShowDotFiles = 1
"
" You can prevent certain files from appearing in the table with the
" following variable:
"
" set wildignore=*.o,*.fasl,CVS
"
" The above will mask all object files, compiled lisp files, and
" files/directories named CVS from appearing in the table. Note that they
" can still be opened by being named explicitly.
"
" See :help 'wildignore' for more information.
"
" Buffer Explorer:
"
" - Buffers are sorted first by fuzzy match and then by most-recently used.
" - The currently active buffer is highlighted.
"
" Buffer Grep:
"
" - Searches all loaded buffers.
" - Uses Ruby-style regexes instead of Vim style. This means:
"
" - \b instead of \< or \> for beginning/end of word.
" - (foo|bar) instead of \(foo\|bar\)
" - {2,5} instead of \{2,5}
" - + instead of \+
" - Generally, fewer backslashes. :-)
"
" - For now, searches are always case-insensitive.
" - Matches from the previous grep are remembered upon relaunch; clear with
" <C-u>.
"
"
" Install Details:
"
" Copy this file into $HOME/.vim/plugin directory so that it will be sourced
" on startup automatically.
"
" Note! This plugin requires Vim be compiled with Ruby interpretation. If you
" don't know if your build of Vim has this functionality, you can check by
" running "vim --version" from the command line and looking for "+ruby".
" Alternatively, just try sourcing this script.
"
" If your version of Vim does not have "+ruby" but you would still like to
" use this plugin, you can fix it. See the "Check for Ruby functionality"
" comment below for instructions.
"
" If you are using the same Vim configuration and plugins for multiple
" machines, some of which have Ruby and some of which don't, you may want to
" turn off the "Sorry, LustyExplorer requires ruby" warning. You can do so
" like this (in .vimrc):
"
" let g:LustyExplorerSuppressRubyWarning = 1
"
"
" Contributing:
"
" Patches and suggestions welcome. Note: lusty-explorer.vim is a generated
" file; if you'd like to submit a patch, check out the Github development
" repository:
"
" http://github.com/sjbach/lusty
"
"
" GetLatestVimScripts: 1890 1 :AutoInstall: lusty-explorer.vim
"
" TODO:
" - when an edited file is in nowrap mode and the explorer is called while the
" current window is scrolled to the right, name truncation occurs.
" - enable VimSwaps stuff
" - set callback when pipe is ready for read and force refresh()
" - uppercase character should make matching case-sensitive
" - FilesystemGrep
" - C-jhkl navigation to highlight a file?
" Exit quickly when already loaded.
if exists("g:loaded_lustyexplorer")
finish
endif
if &compatible
echohl ErrorMsg
echo "LustyExplorer is not designed to run in &compatible mode;"
echo "To use this plugin, first disable vi-compatible mode like so:\n"
echo " :set nocompatible\n"
echo "Or even better, just create an empty .vimrc file."
echohl none
finish
endif
if exists("g:FuzzyFinderMode.TextMate")
echohl WarningMsg
echo "Warning: LustyExplorer detects the presence of fuzzyfinder_textmate;"
echo "that plugin often interacts poorly with other Ruby plugins."
echo "If LustyExplorer gives you an error, you can probably fix it by"
echo "renaming fuzzyfinder_textmate.vim to zzfuzzyfinder_textmate.vim so"
echo "that it is last in the load order."
echohl none
endif
" Check for Ruby functionality.
if !has("ruby") || version < 700
if !exists("g:LustyExplorerSuppressRubyWarning") ||
\ g:LustyExplorerSuppressRubyWarning == "0"
if !exists("g:LustyJugglerSuppressRubyWarning") ||
\ g:LustyJugglerSuppressRubyWarning == "0"
echohl ErrorMsg
echon "Sorry, LustyExplorer requires ruby. "
echon "Here are some tips for adding it:\n"
echo "Debian / Ubuntu:"
echo " # apt-get install vim-ruby\n"
echo "Fedora:"
echo " # yum install vim-enhanced\n"
echo "Gentoo:"
echo " # USE=\"ruby\" emerge vim\n"
echo "FreeBSD:"
echo " # pkg_add -r vim+ruby\n"
echo "Windows:"
echo " 1. Download and install Ruby from here:"
echo " http://www.ruby-lang.org/"
echo " 2. Install a Vim binary with Ruby support:"
echo " http://segfault.hasno.info/vim/gvim72.zip\n"
echo "Manually (including Cygwin):"
echo " 1. Install Ruby."
echo " 2. Download the Vim source package (say, vim-7.0.tar.bz2)"
echo " 3. Build and install:"
echo " # tar -xvjf vim-7.0.tar.bz2"
echo " # ./configure --enable-rubyinterp"
echo " # make && make install"
echo "(If you just wish to stifle this message, set the following option:"
echo " let g:LustyExplorerSuppressRubyWarning = 1)"
echohl none
endif
endif
finish
endif
if ! &hidden
echohl WarningMsg
echo "You are running with 'hidden' mode off. LustyExplorer may"
echo "sometimes emit error messages in this mode -- you should turn"
echo "it on, like so:\n"
echo " :set hidden\n"
echo "Even better, put this in your .vimrc file."
echohl none
endif
let g:loaded_lustyexplorer = "yep"
" Commands.
command LustyBufferExplorer :call <SID>LustyBufferExplorerStart()
command -nargs=? LustyFilesystemExplorer :call <SID>LustyFilesystemExplorerStart("<args>")
command LustyFilesystemExplorerFromHere :call <SID>LustyFilesystemExplorerStart(expand("%:p:h"))
command LustyBufferGrep :call <SID>LustyBufferGrepStart()
" Deprecated command names.
command BufferExplorer :call
\ <SID>deprecated('BufferExplorer', 'LustyBufferExplorer')
command FilesystemExplorer :call
\ <SID>deprecated('FilesystemExplorer', 'LustyFilesystemExplorer')
command FilesystemExplorerFromHere :call
\ <SID>deprecated('FilesystemExplorerFromHere',
\ 'LustyFilesystemExplorerFromHere')
function! s:deprecated(old, new)
echohl WarningMsg
echo ":" . a:old . " is deprecated; use :" . a:new . " instead."
echohl none
endfunction
" Default mappings.
nmap <silent> <Leader>lf :LustyFilesystemExplorer<CR>
nmap <silent> <Leader>lr :LustyFilesystemExplorerFromHere<CR>
nmap <silent> <Leader>lb :LustyBufferExplorer<CR>
nmap <silent> <Leader>lg :LustyBufferGrep<CR>
" Vim-to-ruby function calls.
function! s:LustyFilesystemExplorerStart(path)
exec "ruby LustyE::profile() { $lusty_filesystem_explorer.run_from_path('".a:path."') }"
endfunction
function! s:LustyBufferExplorerStart()
ruby LustyE::profile() { $lusty_buffer_explorer.run }
endfunction
function! s:LustyBufferGrepStart()
ruby LustyE::profile() { $lusty_buffer_grep.run }
endfunction
function! s:LustyFilesystemExplorerCancel()
ruby LustyE::profile() { $lusty_filesystem_explorer.cancel }
endfunction
function! s:LustyBufferExplorerCancel()
ruby LustyE::profile() { $lusty_buffer_explorer.cancel }
endfunction
function! s:LustyBufferGrepCancel()
ruby LustyE::profile() { $lusty_buffer_grep.cancel }
endfunction
function! s:LustyFilesystemExplorerKeyPressed(code_arg)
ruby LustyE::profile() { $lusty_filesystem_explorer.key_pressed }
endfunction
function! s:LustyBufferExplorerKeyPressed(code_arg)
ruby LustyE::profile() { $lusty_buffer_explorer.key_pressed }
endfunction
function! s:LustyBufferGrepKeyPressed(code_arg)
ruby LustyE::profile() { $lusty_buffer_grep.key_pressed }
endfunction
" Setup the autocommands that handle buffer MRU ordering.
augroup LustyExplorer
autocmd!
autocmd BufEnter * ruby LustyE::profile() { $le_buffer_stack.push }
autocmd BufDelete * ruby LustyE::profile() { $le_buffer_stack.pop }
autocmd BufWipeout * ruby LustyE::profile() { $le_buffer_stack.pop }
augroup End
ruby << EOF
require 'pathname'
# For IO#ready -- but Cygwin doesn't have io/wait.
require 'io/wait' unless RUBY_PLATFORM =~ /cygwin/
# Needed for String#each_char in Ruby 1.8 on some platforms.
require 'jcode' unless "".respond_to? :each_char
# Needed for Array#each_slice in Ruby 1.8 on some platforms.
require 'enumerator' unless [].respond_to? :each_slice
$LUSTY_PROFILING = false
if $LUSTY_PROFILING
require 'rubygems'
require 'ruby-prof'
end
module VIM
unless const_defined? "MOST_POSITIVE_INTEGER"
MOST_POSITIVE_INTEGER = 2**(32 - 1) - 2 # Vim ints are signed 32-bit.
end
def self.zero?(var)
# In Vim 7.2 and older, VIM::evaluate returns Strings for boolean
# expressions; in later versions, Fixnums.
case var
when String
var == "0"
when Fixnum
var == 0
else
LustyE::assert(false, "unexpected type: #{var.class}")
end
end
def self.nonzero?(var)
not zero?(var)
end
def self.evaluate_bool(var)
nonzero? evaluate(var)
end
def self.exists?(s)
nonzero? evaluate("exists('#{s}')")
end
def self.has_syntax?
nonzero? evaluate('has("syntax")')
end
def self.has_ext_maparg?
# The 'dict' parameter to mapargs() was introduced in Vim 7.3.32
nonzero? evaluate('v:version > 703 || (v:version == 703 && has("patch32"))')
end
def self.columns
evaluate("&columns").to_i
end
def self.lines
evaluate("&lines").to_i
end
def self.getcwd
evaluate("getcwd()")
end
def self.bufname(i)
if evaluate_bool("empty(bufname(#{i}))")
"<Unknown #{i}>"
else
evaluate("bufname(#{i})")
end
end
def self.single_quote_escape(s)
# Everything in a Vim single-quoted string is literal, except single
# quotes. Single quotes are escaped by doubling them.
s.gsub("'", "''")
end
def self.filename_escape(s)
# Escape slashes, open square braces, spaces, sharps, double quotes and
# percent signs.
s.gsub(/\\/, '\\\\\\').gsub(/[\[ #"%]/, '\\\\\0')
end
def self.regex_escape(s)
s.gsub(/[\]\[.~"^$\\*]/,'\\\\\0')
end
class Buffer
def modified?
VIM::nonzero? VIM::evaluate("getbufvar(#{number()}, '&modified')")
end
def listed?
VIM::nonzero? VIM::evaluate("getbufvar(#{number()}, '&buflisted')")
end
def self.obj_for_bufnr(n)
# There's gotta be a better way to do this...
(0..VIM::Buffer.count-1).each do |i|
obj = VIM::Buffer[i]
return obj if obj.number == n
end
return nil
end
end
# Print with colours
def self.pretty_msg(*rest)
return if rest.length == 0
return if rest.length % 2 != 0
command "redraw" # see :help echo-redraw
i = 0
while i < rest.length do
command "echohl #{rest[i]}"
command "echon '#{rest[i+1]}'"
i += 2
end
command 'echohl None'
end
end
# Hack for wide CJK characters.
if VIM::exists?("*strwidth")
module VIM
def self.strwidth(s)
# strwidth() is defined in Vim 7.3.
evaluate("strwidth('#{single_quote_escape(s)}')").to_i
end
end
else
module VIM
def self.strwidth(s)
s.length
end
end
end
# Utility functions.
module LustyE
unless const_defined? "MOST_POSITIVE_FIXNUM"
MOST_POSITIVE_FIXNUM = 2**(0.size * 8 -2) -1
end
def self.simplify_path(s)
s = s.gsub(/\/+/, '/') # Remove redundant '/' characters
begin
if s[0] == ?~
# Tilde expansion - First expand the ~ part (e.g. '~' or '~steve')
# and then append the rest of the path. We can't just call
# expand_path() or it'll throw on bad paths.
s = File.expand_path(s.sub(/\/.*/,'')) + \
s.sub(/^[^\/]+/,'')
end
if s == '/'
# Special-case root so we don't add superfluous '/' characters,
# as this can make Cygwin choke.
s
elsif ends_with?(s, File::SEPARATOR)
File.expand_path(s) + File::SEPARATOR
else
dirname_expanded = File.expand_path(File.dirname(s))
if dirname_expanded == '/'
dirname_expanded + File.basename(s)
else
dirname_expanded + File::SEPARATOR + File.basename(s)
end
end
rescue ArgumentError
s
end
end
def self.longest_common_prefix(paths)
prefix = paths[0]
paths.each do |path|
for i in 0...prefix.length
if path.length <= i or prefix[i] != path[i]
prefix = prefix[0...i]
prefix = prefix[0..(prefix.rindex('/') or -1)]
break
end
end
end
prefix
end
def self.ready_for_read?(io)
if io.respond_to? :ready?
ready?
else
result = IO.select([io], nil, nil, 0)
result && (result.first.first == io)
end
end
def self.ends_with?(s1, s2)
tail = s1[-s2.length, s2.length]
tail == s2
end
def self.starts_with?(s1, s2)
head = s1[0, s2.length]
head == s2
end
def self.option_set?(opt_name)
opt_name = "g:LustyExplorer" + opt_name
VIM::evaluate_bool("exists('#{opt_name}') && #{opt_name} != '0'")
end
def self.profile
# Profile (if enabled) and provide better
# backtraces when there's an error.
if $LUSTY_PROFILING
if not RubyProf.running?
RubyProf.measure_mode = RubyProf::WALL_TIME
RubyProf.start
else
RubyProf.resume
end
end
begin
yield
rescue Exception => e
puts e
puts e.backtrace
end
if $LUSTY_PROFILING and RubyProf.running?
RubyProf.pause
end
end
class AssertionError < StandardError ; end
def self.assert(condition, message = 'assertion failure')
raise AssertionError.new(message) unless condition
end
def self.d(s)
# (Debug print)
$stderr.puts s
end
end
# Mercury fuzzy matching algorithm, written by Matt Tolton.
# based on the Quicksilver and LiquidMetal fuzzy matching algorithms
class Mercury
public
def self.score(string, abbrev)
return self.new(string, abbrev).score()
end
def score()
return @@SCORE_TRAILING if @abbrev.empty?
return @@SCORE_NO_MATCH if @abbrev.length > @string.length
raw_score = raw_score(0, 0, 0, false)
return raw_score / @string.length
end
def initialize(string, abbrev)
@string = string
@lower_string = string.downcase()
@abbrev = abbrev.downcase()
@level = 0
@branches = 0
end
private
@@SCORE_NO_MATCH = 0.0 # do not change, this is assumed to be 0.0
@@SCORE_EXACT_MATCH = 1.0
@@SCORE_MATCH = 0.9
@@SCORE_TRAILING = 0.7
@@SCORE_TRAILING_BUT_STARTED = 0.80
@@SCORE_BUFFER = 0.70
@@SCORE_BUFFER_BUT_STARTED = 0.80
@@BRANCH_LIMIT = 100
#def raw_score(a, b, c, d)
# @level += 1
# puts "#{' ' * @level}#{a}, #{b}, #{c}, #{d}"
# ret = recurse_and_score(a, b, c, d)
# puts "#{' ' * @level}#{a}, #{b}, #{c}, #{d} -> #{ret}"
# @level -= 1
# return ret
#end
def raw_score(abbrev_idx, match_idx, score_idx, first_char_matched)
index = @lower_string.index(@abbrev[abbrev_idx], match_idx)
return 0.0 if index.nil?
# TODO Instead of having two scores, should there be a sliding "match"
# score based on the distance of the matched character to the beginning
# of the string?
if abbrev_idx == index
score = @@SCORE_EXACT_MATCH
else
score = @@SCORE_MATCH
end
started = (index == 0 or first_char_matched)
# If matching on a word boundary, score the characters since the last match
if index > score_idx
buffer_score = started ? @@SCORE_BUFFER_BUT_STARTED : @@SCORE_BUFFER
if " \t/._-".include?(@string[index - 1])
score += @@SCORE_MATCH
score += buffer_score * ((index - 1) - score_idx)
elsif @string[index] >= "A"[0] and @string[index] <= "Z"[0]
score += buffer_score * (index - score_idx)
end
end
if abbrev_idx + 1 == @abbrev.length
trailing_score = started ? @@SCORE_TRAILING_BUT_STARTED : @@SCORE_TRAILING
# We just matched the last character in the pattern
score += trailing_score * (@string.length - (index + 1))
else
tail_score = raw_score(abbrev_idx + 1, index + 1, index + 1, started)
return 0.0 if tail_score == 0.0
score += tail_score
end
if @branches < @@BRANCH_LIMIT
@branches += 1
alternate = raw_score(abbrev_idx,
index + 1,
score_idx,
first_char_matched)
#puts "#{' ' * @level}#{score}, #{alternate}"
score = [score, alternate].max
end
return score
end
end
module LustyE
# Abstract base class.
class Entry
attr_accessor :full_name, :short_name, :label
def initialize(full_name, short_name, label)
@full_name = full_name
@short_name = short_name
@label = label
end
# NOTE: very similar to BufferStack::shorten_paths()
def self.compute_buffer_entries()
buffer_entries = []
$le_buffer_stack.numbers.each do |n|
o = VIM::Buffer.obj_for_bufnr(n)
next if (o.nil? or not o.listed?)
buffer_entries << self.new(o, n)
end
# Put the current buffer at the end of the list.
buffer_entries << buffer_entries.shift
# Shorten each buffer name by removing all path elements which are not
# needed to differentiate a given name from other names. This usually
# results in only the basename shown, but if several buffers of the
# same basename are opened, there will be more.
# Group the buffers by common basename
common_base = Hash.new { |hash, k| hash[k] = [] }
buffer_entries.each do |entry|
if entry.full_name
basename = Pathname.new(entry.full_name).basename.to_s
common_base[basename] << entry
end
end
# Determine the longest common prefix for each basename group.
basename_to_prefix = {}
common_base.each do |base, entries|
if entries.length > 1
full_names = entries.map { |e| e.full_name }
basename_to_prefix[base] = LustyE::longest_common_prefix(full_names)
end
end
# Compute shortened buffer names by removing prefix, if possible.
buffer_entries.each do |entry|
full_name = entry.full_name
short_name = if full_name.nil?
'[No Name]'
elsif LustyE::starts_with?(full_name, "scp://")
full_name
else
base = Pathname.new(full_name).basename.to_s
prefix = basename_to_prefix[base]
prefix ? full_name[prefix.length..-1] \
: base
end
entry.short_name = short_name
end
buffer_entries
end
end
# Used in FilesystemExplorer
class FilesystemEntry < Entry
attr_accessor :current_score
def initialize(label)
super("::UNSET::", "::UNSET::", label)
@current_score = 0.0
end
end
# Used in BufferExplorer
class BufferEntry < Entry
attr_accessor :vim_buffer, :mru_placement, :current_score
def initialize(vim_buffer, mru_placement)
super(vim_buffer.name, "::UNSET::", "::UNSET::")
@vim_buffer = vim_buffer
@mru_placement = mru_placement
@current_score = 0.0
end
end
# Used in BufferGrep
class GrepEntry < Entry
attr_accessor :vim_buffer, :mru_placement, :line_number
def initialize(vim_buffer, mru_placement)
super(vim_buffer.name, "::UNSET::", "::UNSET::")
@vim_buffer = vim_buffer
@mru_placement = mru_placement
@line_number = 0
end
end
end
# Abstract base class; extended as BufferExplorer, FilesystemExplorer
module LustyE
class Explorer
public
def initialize
@settings = SavedSettings.new
@display = Display.new title()
@prompt = nil
@current_sorted_matches = []
@running = false
end
def run
return if @running
@settings.save
@running = true
@calling_window = $curwin
@saved_alternate_bufnum = if VIM::evaluate_bool("expand('#') == ''")
nil
else
VIM::evaluate("bufnr(expand('#'))")
end
create_explorer_window()
refresh(:full)
end
def key_pressed()
# Grab argument from the Vim function.
i = VIM::evaluate("a:code_arg").to_i
refresh_mode = :full
case i
when 32..126 # Printable characters
c = i.chr
@prompt.add! c
@selected_index = 0
when 8 # Backspace/Del/C-h
@prompt.backspace!
@selected_index = 0
when 9, 13 # Tab and Enter
choose(:current_tab)
when 23 # C-w (delete 1 dir backward)
@prompt.up_one_dir!
@selected_index = 0
when 14 # C-n (select next)
@selected_index = \
if @current_sorted_matches.size.zero?
0
else
(@selected_index + 1) % @current_sorted_matches.size
end
refresh_mode = :no_recompute
when 16 # C-p (select previous)
@selected_index = \
if @current_sorted_matches.size.zero?
0
else
(@selected_index - 1) % @current_sorted_matches.size
end
refresh_mode = :no_recompute
when 6 # C-f (select right)
@selected_index = \
if @row_count.nil? || @row_count.zero?
0
else
columns = \
(@current_sorted_matches.size.to_f / @row_count.to_f).ceil
cur_column = @selected_index / @row_count
cur_row = @selected_index % @row_count
new_column = (cur_column + 1) % columns
if (new_column + 1) * (cur_row + 1) > @current_sorted_matches.size
new_column = 0
end
new_column * @row_count + cur_row
end
refresh_mode = :no_recompute
when 2 # C-b (select left)
@selected_index = \
if @row_count.nil? || @row_count.zero?
0
else
columns = \
(@current_sorted_matches.size.to_f / @row_count.to_f).ceil
cur_column = @selected_index / @row_count
cur_row = @selected_index % @row_count
new_column = (cur_column - 1) % columns
if (new_column + 1) * (cur_row + 1) > @current_sorted_matches.size
new_column = columns - 2
end
new_column * @row_count + cur_row
end
refresh_mode = :no_recompute
when 15 # C-o choose in new horizontal split
choose(:new_split)
when 20 # C-t choose in new tab
choose(:new_tab)
when 21 # C-u clear prompt
@prompt.clear!
@selected_index = 0
when 22 # C-v choose in new vertical split
choose(:new_vsplit)
end
refresh(refresh_mode)
end
def cancel
if @running
cleanup()
# fix alternate file
if @saved_alternate_bufnum
cur = $curbuf
VIM::command "silent b #{@saved_alternate_bufnum}"
VIM::command "silent b #{cur.number}"
end
if $LUSTY_PROFILING
outfile = File.new('lusty-explorer-rbprof.html', 'a')
#RubyProf::CallTreePrinter.new(RubyProf.stop).print(outfile)
RubyProf::GraphHtmlPrinter.new(RubyProf.stop).print(outfile)
end
end
end
private
def refresh(mode)
return if not @running
if mode == :full
@current_sorted_matches = compute_sorted_matches()
end
on_refresh()
highlight_selected_index() if VIM::has_syntax?
@row_count = @display.print @current_sorted_matches.map { |x| x.label }
@prompt.print Display.max_width
end
def create_explorer_window
# Trim out the "::" in "LustyE::FooExplorer"
key_binding_prefix = 'Lusty' + self.class.to_s.sub(/.*::/,'')
@display.create(key_binding_prefix)
set_syntax_matching()
end
def highlight_selected_index
# Note: overridden by BufferGrep
VIM::command 'syn clear LustySelected'
entry = @current_sorted_matches[@selected_index]
return if entry.nil?
escaped = VIM::regex_escape(entry.label)
label_match_string = Display.entry_syntaxify(escaped, false)
VIM::command "syn match LustySelected \"#{label_match_string}\" " \
'contains=LustyGrepMatch'
end
def choose(open_mode)
entry = @current_sorted_matches[@selected_index]
return if entry.nil?
open_entry(entry, open_mode)
end
def cleanup
@display.close
Window.select @calling_window
@settings.restore
@running = false
VIM::message ""
LustyE::assert(@calling_window == $curwin)
end
# Pure virtual methods
# - set_syntax_matching
# - on_refresh
# - open_entry
# - compute_sorted_matches
end
end
module LustyE
class BufferExplorer < Explorer
public
def initialize
super
@prompt = Prompt.new
@buffer_entries = []
end
def run
unless @running
@prompt.clear!
@curbuf_at_start = VIM::Buffer.current
@buffer_entries = BufferEntry::compute_buffer_entries()
@buffer_entries.each do |e|
# Show modification indicator
e.label = e.short_name
e.label << " [+]" if e.vim_buffer.modified?
# Disabled: show buffer number next to name
#e.label << " #{buffer.number.to_s}"
end
@selected_index = 0
super
end
end
private
def title
'[LustyExplorer-Buffers]'
end
def set_syntax_matching
# Base highlighting -- more is set on refresh.
if VIM::has_syntax?
VIM::command 'syn match LustySlash "/" contained'
VIM::command 'syn match LustyDir "\%(\S\+ \)*\S\+/" ' \
'contains=LustySlash'
VIM::command 'syn match LustyModified " \[+\]"'
end
end
def curbuf_match_string
curbuf = @buffer_entries.find { |x| x.vim_buffer == @curbuf_at_start }
if curbuf
escaped = VIM::regex_escape(curbuf.label)
Display.entry_syntaxify(escaped, @prompt.insensitive?)
else
""
end
end
def on_refresh
# Highlighting for the current buffer name.
if VIM::has_syntax?
VIM::command 'syn clear LustyCurrentBuffer'
VIM::command 'syn match LustyCurrentBuffer ' \
"\"#{curbuf_match_string()}\" " \
'contains=LustyModified'
end
end
def current_abbreviation
@prompt.input
end
def compute_sorted_matches
abbrev = current_abbreviation()
if abbrev.length == 0
# Take (current) MRU order if we have no abbreviation.
@buffer_entries
else
matching_entries = \
@buffer_entries.select { |x|
x.current_score = Mercury.score(x.short_name, abbrev)
x.current_score != 0.0
}
# Sort by score.
matching_entries.sort! { |x, y|
if x.current_score == y.current_score
x.mru_placement <=> y.mru_placement
else
y.current_score <=> x.current_score
end
}
end
end
def open_entry(entry, open_mode)
cleanup()
LustyE::assert($curwin == @calling_window)
number = entry.vim_buffer.number
LustyE::assert(number)
cmd = case open_mode
when :current_tab
"b"
when :new_tab
# For some reason just using tabe or e gives an error when
# the alternate-file isn't set.
"tab split | b"
when :new_split
"sp | b"
when :new_vsplit
"vs | b"
else
LustyE::assert(false, "bad open mode")
end
VIM::command "silent #{cmd} #{number}"
end
end
end
module LustyE
class FilesystemExplorer < Explorer
public
def initialize
super
@prompt = FilesystemPrompt.new
@memoized_dir_contents = {}
end
def run
return if @running
FileMasks.create_glob_masks()
@vim_swaps = VimSwaps.new
@selected_index = 0
super
end
def run_from_path(path)
return if @running
if path.empty?
path = VIM::getcwd()
end
if path.respond_to?(:force_encoding)
path = path.force_encoding(VIM::evaluate('&enc'))
end
@prompt.set!(path + File::SEPARATOR)
run()
end
def key_pressed()
i = VIM::evaluate("a:code_arg").to_i
case i
when 1, 10 # <C-a>, <Shift-Enter>
cleanup()
# Open all non-directories currently in view.
@current_sorted_matches.each do |e|
path_str = \
if @prompt.at_dir?
@prompt.input + e.label
else
dir = @prompt.dirname
if dir == '/'
dir + e.label
else
dir + File::SEPARATOR + e.label
end
end
load_file(path_str, :current_tab) unless File.directory?(path_str)
end
when 5 # <C-e> edit file, create it if necessary
if not @prompt.at_dir?
cleanup()
# Force a reread of this directory so that the new file will
# show up (as long as it is saved before the next run).
@memoized_dir_contents.delete(view_path())
load_file(@prompt.input, :current_tab)
end
when 18 # <C-r> refresh
@memoized_dir_contents.delete(view_path())
refresh(:full)
else
super
end
end
private
def title
'[LustyExplorer-Files]'
end
def set_syntax_matching
# Base highlighting -- more is set on refresh.
if VIM::has_syntax?
VIM::command 'syn match LustySlash "/" contained'
VIM::command 'syn match LustyDir "\%(\S\+ \)*\S\+/" ' \
'contains=LustySlash'
end
end
def on_refresh
if VIM::has_syntax?
VIM::command 'syn clear LustyFileWithSwap'
view = view_path()
@vim_swaps.file_names.each do |file_with_swap|
if file_with_swap.dirname == view
base = file_with_swap.basename
escaped = VIM::regex_escape(base.to_s)
match_str = Display.entry_syntaxify(escaped, false)
VIM::command "syn match LustyFileWithSwap \"#{match_str}\""
end
end
end
# TODO: restore highlighting for open buffers?
end
def current_abbreviation
if @prompt.at_dir?
""
else
File.basename(@prompt.input)
end
end
def view_path
input = @prompt.input
path = \
if @prompt.at_dir? and \
input.length > 1 # Not root
# The last element in the path is a directory + '/' and we want to
# see what's in it instead of what's in its parent directory.
Pathname.new(input[0..-2]) # Canonicalize by removing trailing '/'
else
Pathname.new(input).dirname
end
return path
end
def all_files_at_view
view = view_path()
unless @memoized_dir_contents.has_key?(view)
if not view.directory?
return []
elsif not view.readable?
# TODO: show "-- PERMISSION DENIED --"
return []
end
# Generate an array of the files
entries = []
view_str = view.to_s
unless LustyE::ends_with?(view_str, File::SEPARATOR)
# Don't double-up on '/' -- makes Cygwin sad.
view_str << File::SEPARATOR
end
Dir.foreach(view_str) do |name|
next if name == "." # Skip pwd
next if name == ".." and LustyE::option_set?("AlwaysShowDotFiles")
# Hide masked files.
next if FileMasks.masked?(name)
if FileTest.directory?(view_str + name)
name << File::SEPARATOR
end
entries << FilesystemEntry.new(name)
end
@memoized_dir_contents[view] = entries
end
all = @memoized_dir_contents[view]
if LustyE::option_set?("AlwaysShowDotFiles") or \
current_abbreviation()[0] == ?.
all
else
# Filter out dotfiles if the current abbreviation doesn't start with
# '.'.
all.select { |x| x.label[0] != ?. }
end
end
def compute_sorted_matches
abbrev = current_abbreviation()
unsorted = all_files_at_view()
if abbrev.length == 0
# Sort alphabetically if we have no abbreviation.
unsorted.sort { |x, y| x.label <=> y.label }
else
matches = \
unsorted.select { |x|
x.current_score = Mercury.score(x.label, abbrev)
x.current_score != 0.0
}
if abbrev == '.'
# Sort alphabetically, otherwise it just looks weird.
matches.sort! { |x, y| x.label <=> y.label }
else
# Sort by score.
matches.sort! { |x, y| y.current_score <=> x.current_score }
end
end
end
def open_entry(entry, open_mode)
path = view_path() + entry.label
if File.directory?(path)
# Recurse into the directory instead of opening it.
@prompt.set!(path.to_s)
@selected_index = 0
elsif entry.label.include?(File::SEPARATOR)
# Don't open a fake file/buffer with "/" in its name.
return
else
cleanup()
load_file(path.to_s, open_mode)
end
end
def load_file(path_str, open_mode)
LustyE::assert($curwin == @calling_window)
# Escape for Vim and remove leading ./ for files in pwd.
filename_escaped = VIM::filename_escape(path_str).sub(/^\.\//,"")
single_quote_escaped = VIM::single_quote_escape(filename_escaped)
sanitized = VIM::evaluate "fnamemodify('#{single_quote_escaped}', ':.')"
cmd = case open_mode
when :current_tab
"e"
when :new_tab
"tabe"
when :new_split
"sp"
when :new_vsplit
"vs"
else
LustyE::assert(false, "bad open mode")
end
VIM::command "silent #{cmd} #{sanitized}"
end
end
end
# TODO:
# - some way for user to indicate case-sensitive regex
# - add slash highlighting back to file name?
module LustyE
class BufferGrep < Explorer
public
def initialize
super
@display.single_column_mode = true
@prompt = Prompt.new
@buffer_entries = []
@matched_strings = []
# State from previous run, so you don't have to retype
# your search each time to get the previous entries.
@previous_input = ''
@previous_grep_entries = []
@previous_matched_strings = []
@previous_selected_index = 0
end
def run
return if @running
@prompt.set! @previous_input
@buffer_entries = GrepEntry::compute_buffer_entries()
@selected_index = @previous_selected_index
super
end
private
def title
'[LustyExplorer-BufferGrep]'
end
def set_syntax_matching
VIM::command 'syn clear LustyGrepFileName'
VIM::command 'syn clear LustyGrepLineNumber'
VIM::command 'syn clear LustyGrepContext'
# Base syntax matching -- others are set on refresh.
VIM::command \
'syn match LustyGrepFileName "^\zs.\{-}\ze:\d\+:" ' \
'contains=NONE ' \
'nextgroup=LustyGrepLineNumber'
VIM::command \
'syn match LustyGrepLineNumber ":\d\+:" ' \
'contained ' \
'contains=NONE ' \
'nextgroup=LustyGrepContext'
VIM::command \
'syn match LustyGrepContext ".*" ' \
'transparent ' \
'contained ' \
'contains=LustyGrepMatch'
end
def on_refresh
if VIM::has_syntax?
VIM::command 'syn clear LustyGrepMatch'
if not @matched_strings.empty?
sub_regexes = @matched_strings.map { |s| VIM::regex_escape(s) }
syntax_regex = '\%(' + sub_regexes.join('\|') + '\)'
VIM::command "syn match LustyGrepMatch \"#{syntax_regex}\" " \
"contained " \
"contains=NONE"
end
end
end
def highlight_selected_index
VIM::command 'syn clear LustySelected'
entry = @current_sorted_matches[@selected_index]
return if entry.nil?
match_string = "#{entry.short_name}:#{entry.line_number}:"
escaped = VIM::regex_escape(match_string)
VIM::command "syn match LustySelected \"^#{match_string}\" " \
'contains=NONE ' \
'nextgroup=LustyGrepContext'
end
def current_abbreviation
@prompt.input
end
def compute_sorted_matches
abbrev = current_abbreviation()
grep_entries = @previous_grep_entries
@matched_strings = @previous_matched_strings
@previous_input = ''
@previous_grep_entries = []
@previous_matched_strings = []
@previous_selected_index = 0
if not grep_entries.empty?
return grep_entries
elsif abbrev == ''
@buffer_entries.each do |e|
e.label = e.short_name
end
return @buffer_entries
end
begin
regex = Regexp.compile(abbrev, Regexp::IGNORECASE)
rescue RegexpError => e
return []
end
max_visible_entries = Display.max_height
# Used to avoid duplicating match strings, which slows down refresh.
highlight_hash = {}
# Search through every line of every open buffer for the
# given expression.
@buffer_entries.each do |entry|
vim_buffer = entry.vim_buffer
line_count = vim_buffer.count
(1..line_count). each do |i|
line = vim_buffer[i]
match = regex.match(line)
if match
matched_str = match.to_s
grep_entry = entry.clone()
grep_entry.line_number = i
grep_entry.label = "#{grep_entry.short_name}:#{i}:#{line}"
grep_entries << grep_entry
# Keep track of all matched strings
unless highlight_hash[matched_str]
@matched_strings << matched_str
highlight_hash[matched_str] = true
end
if grep_entries.length > max_visible_entries
return grep_entries
end
end
end
end
return grep_entries
end
def open_entry(entry, open_mode)
cleanup()
LustyE::assert($curwin == @calling_window)
number = entry.vim_buffer.number
LustyE::assert(number)
cmd = case open_mode
when :current_tab
"b"
when :new_tab
# For some reason just using tabe or e gives an error when
# the alternate-file isn't set.
"tab split | b"
when :new_split
"sp | b"
when :new_vsplit
"vs | b"
else
LustyE::assert(false, "bad open mode")
end
# Open buffer and go to the line number.
VIM::command "silent #{cmd} #{number}"
VIM::command "#{entry.line_number}"
end
def cleanup
@previous_input = @prompt.input
@previous_grep_entries = @current_sorted_matches
@previous_matched_strings = @matched_strings
@previous_selected_index = @selected_index
super
end
end
end
module LustyE
# Used in BufferExplorer
class Prompt
private
@@PROMPT = ">> "
public
def initialize
clear!
end
def clear!
@input = ""
end
def print(max_width = 0)
text = @input
# may need some extra characters for "..." and spacing
max_width -= 5
if max_width > 0 && text.length > max_width
text = "..." + text[(text.length - max_width + 3 ) .. -1]
end
VIM::pretty_msg("Comment", @@PROMPT,
"None", VIM::single_quote_escape(text),
"Underlined", " ")
end
def set!(s)
@input = s
end
def input
@input
end
def insensitive?
@input == @input.downcase
end
def ends_with?(c)
LustyE::ends_with?(@input, c)
end
def add!(s)
@input << s
end
def backspace!
@input.chop!
end
def up_one_dir!
@input.chop!
while !@input.empty? and @input[-1] != ?/
@input.chop!
end
end
end
# Used in FilesystemExplorer
class FilesystemPrompt < Prompt
def initialize
super
@memoized = nil
@dirty = true
end
def clear!
super
@dirty = true
end
def set!(s)
# On Windows, Vim will return paths with a '\' separator, but
# we want to use '/'.
super(s.gsub('\\', '/'))
@dirty = true
end
def backspace!
super
@dirty = true
end
def up_one_dir!
super
@dirty = true
end
def at_dir?
# We have not typed anything yet or have just typed the final '/' on a
# directory name in pwd. This check is interspersed throughout
# FilesystemExplorer because of the conventions of basename and dirname.
input().empty? or input()[-1] == File::SEPARATOR[0]
# Don't think the File.directory? call is necessary, but leaving this
# here as a reminder.
#(File.directory?(input()) and input().ends_with?(File::SEPARATOR))
end
def insensitive?
at_dir? or (basename() == basename().downcase)
end
def add!(s)
# Assumption: add!() will only receive enough chars at a time to complete
# a single directory level, e.g. foo/, not foo/bar/
@input << s
@dirty = true
end
def input
if @dirty
@memoized = LustyE::simplify_path(variable_expansion(@input))
@dirty = false
end
@memoized
end
def basename
File.basename input()
end
def dirname
File.dirname input()
end
private
def variable_expansion (input_str)
strings = input_str.split('$', -1)
return "" if strings.nil? or strings.length == 0
first = strings.shift
# Try to expand each instance of $<word>.
strings.inject(first) { |str, s|
if s =~ /^(\w+)/ and ENV[$1]
str + s.sub($1, ENV[$1])
else
str + "$" + s
end
}
end
end
end
# Simplify switching between windows.
module LustyE
class Window
def self.select(window)
return true if window == $curwin
start = $curwin
# Try to select the given window.
begin
VIM::command "wincmd w"
end while ($curwin != window) and ($curwin != start)
if $curwin == window
return true
else
# Failed -- re-select the starting window.
VIM::command("wincmd w") while $curwin != start
VIM::pretty_msg("ErrorMsg", "Cannot find the correct window!")
return false
end
end
end
end
# Save and restore settings when creating the explorer buffer.
module LustyE
class SavedSettings
def initialize
save()
end
def save
@timeoutlen = VIM::evaluate("&timeoutlen")
@splitbelow = VIM::evaluate_bool("&splitbelow")
@insertmode = VIM::evaluate_bool("&insertmode")
@showcmd = VIM::evaluate_bool("&showcmd")
@list = VIM::evaluate_bool("&list")
@report = VIM::evaluate("&report")
@sidescroll = VIM::evaluate("&sidescroll")
@sidescrolloff = VIM::evaluate("&sidescrolloff")
VIM::command "let s:win_size_restore = winrestcmd()"
end
def restore
VIM::set_option "timeoutlen=#{@timeoutlen}"
if @splitbelow
VIM::set_option "splitbelow"
else
VIM::set_option "nosplitbelow"
end
if @insertmode
VIM::set_option "insertmode"
else
VIM::set_option "noinsertmode"
end
if @showcmd
VIM::set_option "showcmd"
else
VIM::set_option "noshowcmd"
end
if @list
VIM::set_option "list"
else
VIM::set_option "nolist"
end
VIM::command "set report=#{@report}"
VIM::command "set sidescroll=#{@sidescroll}"
VIM::command "set sidescrolloff=#{@sidescrolloff}"
VIM::command "exe s:win_size_restore"
end
end
end
# Manage the explorer buffer.
module LustyE
class Display
private
@@COLUMN_SEPARATOR = " "
@@NO_MATCHES_STRING = "-- NO MATCHES --"
@@TRUNCATED_STRING = "-- TRUNCATED --"
public
ENTRY_START_VIM_REGEX = '\%(^\|' + @@COLUMN_SEPARATOR + '\)'
ENTRY_END_VIM_REGEX = '\%(\s*$\|' + @@COLUMN_SEPARATOR + '\)'
def self.entry_syntaxify(s, case_insensitive)
# Create a match regex string for the given s. This is for a Vim regex,
# not for a Ruby regex.
str = "#{ENTRY_START_VIM_REGEX}\\zs#{s}\\ze#{ENTRY_END_VIM_REGEX}"
str << '\c' if case_insensitive
return str
end
attr_writer :single_column_mode
def initialize(title)
@title = title
@window = nil
@buffer = nil
@single_column_mode = false
end
def create(prefix)
# Make a window for the display and move there.
# Start at size 1 to mitigate flashing effect when
# we resize the window later.
VIM::command "silent! botright 1split #{@title}"
@window = $curwin
@buffer = $curbuf
#
# Display buffer is special -- set options.
#
# Buffer-local.
VIM::command "setlocal bufhidden=delete"
VIM::command "setlocal buftype=nofile"
VIM::command "setlocal nomodifiable"
VIM::command "setlocal noswapfile"
VIM::command "setlocal nowrap"
VIM::command "setlocal nonumber"
VIM::command "setlocal foldcolumn=0"
VIM::command "setlocal nocursorline"
VIM::command "setlocal nospell"
VIM::command "setlocal nobuflisted"
VIM::command "setlocal textwidth=0"
VIM::command "setlocal noreadonly"
# Non-buffer-local (Vim is annoying).
# (Update SavedSettings if adding to below.)
VIM::set_option "timeoutlen=0"
VIM::set_option "noinsertmode"
VIM::set_option "noshowcmd"
VIM::set_option "nolist"
VIM::set_option "report=9999"
VIM::set_option "sidescroll=0"
VIM::set_option "sidescrolloff=0"
# TODO -- cpoptions?
#
# Syntax highlighting.
#
if VIM::has_syntax?
# General syntax matching.
VIM::command 'syn match LustyNoEntries "\%^\s*' \
"#{@@NO_MATCHES_STRING}" \
'\s*\%$"'
VIM::command 'syn match LustyTruncated "^\s*' \
"#{@@TRUNCATED_STRING}" \
'\s*$"'
# Colour highlighting.
VIM::command 'highlight link LustyDir Directory'
VIM::command 'highlight link LustySlash Function'
VIM::command 'highlight link LustySelected Type'
VIM::command 'highlight link LustyModified Special'
VIM::command 'highlight link LustyCurrentBuffer Constant'
VIM::command 'highlight link LustyGrepMatch IncSearch'
VIM::command 'highlight link LustyGrepLineNumber Directory'
VIM::command 'highlight link LustyGrepFileName Comment'
VIM::command 'highlight link LustyGrepContext None' # transparent
VIM::command 'highlight link LustyOpenedFile PreProc'
VIM::command 'highlight link LustyFileWithSwap WarningMsg'
VIM::command 'highlight link LustyNoEntries ErrorMsg'
VIM::command 'highlight link LustyTruncated Visual'
if VIM::exists? '*clearmatches'
VIM::evaluate 'clearmatches()'
end
end
#
# Key mappings - we need to reroute user input.
#
# Non-special printable characters.
printables = '/!"#$%&\'()*+,-.0123456789:<=>?#@"' \
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
'[]^_`abcdefghijklmnopqrstuvwxyz{}~'
map = "noremap <silent> <buffer>"
printables.each_byte do |b|
VIM::command "#{map} <Char-#{b}> :call <SID>#{prefix}KeyPressed(#{b})<CR>"
end
# Special characters
VIM::command "#{map} <Tab> :call <SID>#{prefix}KeyPressed(9)<CR>"
VIM::command "#{map} <Bslash> :call <SID>#{prefix}KeyPressed(92)<CR>"
VIM::command "#{map} <Space> :call <SID>#{prefix}KeyPressed(32)<CR>"
VIM::command "#{map} \026| :call <SID>#{prefix}KeyPressed(124)<CR>"
VIM::command "#{map} <BS> :call <SID>#{prefix}KeyPressed(8)<CR>"
VIM::command "#{map} <Del> :call <SID>#{prefix}KeyPressed(8)<CR>"
VIM::command "#{map} <C-h> :call <SID>#{prefix}KeyPressed(8)<CR>"
VIM::command "#{map} <CR> :call <SID>#{prefix}KeyPressed(13)<CR>"
VIM::command "#{map} <S-CR> :call <SID>#{prefix}KeyPressed(10)<CR>"
VIM::command "#{map} <C-a> :call <SID>#{prefix}KeyPressed(1)<CR>"
VIM::command "#{map} <Esc> :call <SID>#{prefix}Cancel()<CR>"
VIM::command "#{map} <C-c> :call <SID>#{prefix}Cancel()<CR>"
VIM::command "#{map} <C-g> :call <SID>#{prefix}Cancel()<CR>"
VIM::command "#{map} <C-w> :call <SID>#{prefix}KeyPressed(23)<CR>"
VIM::command "#{map} <C-n> :call <SID>#{prefix}KeyPressed(14)<CR>"
VIM::command "#{map} <C-p> :call <SID>#{prefix}KeyPressed(16)<CR>"
VIM::command "#{map} <C-f> :call <SID>#{prefix}KeyPressed(6)<CR>"
VIM::command "#{map} <C-b> :call <SID>#{prefix}KeyPressed(2)<CR>"
VIM::command "#{map} <C-o> :call <SID>#{prefix}KeyPressed(15)<CR>"
VIM::command "#{map} <C-t> :call <SID>#{prefix}KeyPressed(20)<CR>"
VIM::command "#{map} <C-v> :call <SID>#{prefix}KeyPressed(22)<CR>"
VIM::command "#{map} <C-e> :call <SID>#{prefix}KeyPressed(5)<CR>"
VIM::command "#{map} <C-r> :call <SID>#{prefix}KeyPressed(18)<CR>"
VIM::command "#{map} <C-u> :call <SID>#{prefix}KeyPressed(21)<CR>"
VIM::command "#{map} <Esc>OD :call <SID>#{prefix}KeyPressed(2)<CR>"
VIM::command "#{map} <Esc>OC :call <SID>#{prefix}KeyPressed(6)<CR>"
VIM::command "#{map} <Esc>OA :call <SID>#{prefix}KeyPressed(16)<CR>"
VIM::command "#{map} <Esc>OB :call <SID>#{prefix}KeyPressed(14)<CR>"
VIM::command "#{map} <Left> :call <SID>#{prefix}KeyPressed(2)<CR>"
VIM::command "#{map} <Right> :call <SID>#{prefix}KeyPressed(6)<CR>"
VIM::command "#{map} <Up> :call <SID>#{prefix}KeyPressed(16)<CR>"
VIM::command "#{map} <Down> :call <SID>#{prefix}KeyPressed(14)<CR>"
end
def print(strings)
Window.select(@window) || return
if strings.empty?
print_no_entries()
return
end
row_count, col_count, col_widths, truncated = \
compute_optimal_layout(strings)
# Slice the strings into rows.
rows = Array.new(row_count){[]}
col_index = 0
strings.each_slice(row_count) do |column|
column_width = col_widths[col_index]
column.each_index do |i|
string = column[i]
rows[i] << string
if col_index < col_count - 1
# Add spacer to the width of the column
rows[i] << (" " * (column_width - VIM::strwidth(string)))
rows[i] << @@COLUMN_SEPARATOR
end
end
col_index += 1
break if col_index >= col_count
end
print_rows(rows, truncated)
row_count
end
def close
# Only wipe the buffer if we're *sure* it's the explorer.
if Window.select @window and \
$curbuf == @buffer and \
$curbuf.name =~ /#{Regexp.escape(@title)}$/
VIM::command "bwipeout!"
@window = nil
@buffer = nil
end
end
def self.max_height
stored_height = $curwin.height
$curwin.height = VIM::MOST_POSITIVE_INTEGER
highest_allowable = $curwin.height
$curwin.height = stored_height
highest_allowable
end
def self.max_width
VIM::columns()
end
private
def compute_optimal_layout(strings)
# Compute optimal row count and corresponding column count.
# The display attempts to fit `strings' on as few rows as
# possible.
max_width = Display.max_width()
max_height = Display.max_height()
displayable_string_upper_bound = compute_displayable_upper_bound(strings)
# Determine optimal row count.
optimal_row_count, truncated = \
if @single_column_mode
if strings.length <= max_height
[strings.length, false]
else
[max_height - 1, true]
end
elsif strings.length > displayable_string_upper_bound
# Use all available rows and truncate results.
# The -1 is for the truncation indicator.
[Display.max_height - 1, true]
else
single_row_width = \
strings.inject(0) { |len, s|
len + @@COLUMN_SEPARATOR.length + s.length
}
if single_row_width <= max_width or \
strings.length == 1
# All fits on a single row
[1, false]
else
compute_optimal_row_count(strings)
end
end
# Compute column_count and column_widths.
column_count = 0
column_widths = []
total_width = 0
strings.each_slice(optimal_row_count) do |column|
longest = column.max { |a, b| VIM::strwidth(a) <=> VIM::strwidth(b) }
column_width = VIM::strwidth(longest)
total_width += column_width
break if total_width > max_width
column_count += 1
column_widths << column_width
total_width += @@COLUMN_SEPARATOR.length
end
[optimal_row_count, column_count, column_widths, truncated]
end
def print_rows(rows, truncated)
unlock_and_clear()
# Grow/shrink the window as needed
$curwin.height = rows.length + (truncated ? 1 : 0)
# Print the rows.
rows.each_index do |i|
$curwin.cursor = [i+1, 1]
$curbuf.append(i, rows[i].join(''))
end
# Print a TRUNCATED indicator, if needed.
if truncated
$curbuf.append($curbuf.count - 1, \
@@TRUNCATED_STRING.center($curwin.width, " "))
end
# Stretch the last line to the length of the window with whitespace so
# that we can "hide" the cursor in the corner.
last_line = $curbuf[$curbuf.count - 1]
last_line << (" " * [$curwin.width - last_line.length,0].max)
$curbuf[$curbuf.count - 1] = last_line
# There's a blank line at the end of the buffer because of how
# VIM::Buffer.append works.
$curbuf.delete $curbuf.count
lock()
end
def print_no_entries
unlock_and_clear()
$curwin.height = 1
$curbuf[1] = @@NO_MATCHES_STRING.center($curwin.width, " ")
lock()
end
def unlock_and_clear
VIM::command "setlocal modifiable"
# Clear the explorer (black hole register)
VIM::command "silent %d _"
end
def lock
VIM::command "setlocal nomodifiable"
# Hide the cursor
VIM::command "normal! Gg$"
end
def compute_displayable_upper_bound(strings)
# Compute an upper-bound on the number of displayable matches.
# Basically: find the length of the longest string, then keep
# adding shortest strings until we pass the width of the Vim
# window. This is the maximum possible column-count assuming
# all strings can fit. Then multiply by the number of rows.
sorted_by_shortest = strings.sort { |x, y| x.length <=> y.length }
longest_length = sorted_by_shortest.pop.length
row_width = longest_length + @@COLUMN_SEPARATOR.length
max_width = Display.max_width()
column_count = 1
sorted_by_shortest.each do |str|
row_width += str.length
if row_width > max_width
break
end
column_count += 1
row_width += @@COLUMN_SEPARATOR.length
end
column_count * Display.max_height()
end
def compute_optimal_row_count(strings)
max_width = Display.max_width
max_height = Display.max_height
# Hashes by range, e.g. 0..2, representing the width
# of the column bounded by that range.
col_range_widths = {}
# Binary search; find the lowest number of rows at which we
# can fit all the strings.
# We've already failed for a single row, so start at two.
lower = 1 # (1 = 2 - 1)
upper = max_height + 1
while lower + 1 != upper
row_count = (lower + upper) / 2 # Mid-point
col_start_index = 0
col_end_index = row_count - 1
total_width = 0
while col_end_index < strings.length
total_width += \
compute_column_width(col_start_index..col_end_index,
strings, col_range_widths)
if total_width > max_width
# Early exit.
total_width = LustyE::MOST_POSITIVE_FIXNUM
break
end
total_width += @@COLUMN_SEPARATOR.length
col_start_index += row_count
col_end_index += row_count
if col_end_index >= strings.length and \
col_start_index < strings.length
# Remainder; last iteration will not be a full column.
col_end_index = strings.length - 1
end
end
# The final column doesn't need a separator.
total_width -= @@COLUMN_SEPARATOR.length
if total_width <= max_width
# This row count fits.
upper = row_count
else
# This row count doesn't fit.
lower = row_count
end
end
if upper > max_height
# No row count can accomodate all strings; have to truncate.
# (-1 for the truncate indicator)
[max_height - 1, true]
else
[upper, false]
end
end
def compute_column_width(range, strings, col_range_widths)
if (range.first == range.last)
return strings[range.first].length
end
width = col_range_widths[range]
if width.nil?
# Recurse for each half of the range.
split_point = range.first + ((range.last - range.first) >> 1)
first_half = compute_column_width(range.first..split_point,
strings, col_range_widths)
second_half = compute_column_width(split_point+1..range.last,
strings, col_range_widths)
width = [first_half, second_half].max
col_range_widths[range] = width
end
width
end
end
end
module LustyE
class FileMasks
private
@@glob_masks = []
public
def FileMasks.create_glob_masks
@@glob_masks = \
if VIM::exists? "g:LustyExplorerFileMasks"
# Note: this variable deprecated.
VIM::evaluate("g:LustyExplorerFileMasks").split(',')
elsif VIM::exists? "&wildignore"
VIM::evaluate("&wildignore").split(',')
else
[]
end
end
def FileMasks.masked?(str)
@@glob_masks.each do |mask|
return true if File.fnmatch(mask, str)
end
return false
end
end
end
module LustyE
class VimSwaps
def initialize
if VIM::has_syntax?
# FIXME: vvv disabled
# @vim_r = IO.popen("vim -r --noplugin -i NONE 2>&1")
# @files_with_swaps = nil
@files_with_swaps = []
else
@files_with_swaps = []
end
end
def file_names
if @files_with_swaps.nil?
if LustyE::ready_for_read?(@vim_r)
@files_with_swaps = []
@vim_r.each_line do |line|
if line =~ /^ +file name: (.*)$/
file = $1.chomp
@files_with_swaps << Pathname.new(LustyE::simplify_path(file))
end
end
else
return []
end
end
@files_with_swaps
end
end
end
# Maintain MRU ordering.
module LustyE
class BufferStack
public
def initialize
@stack = []
(0..VIM::Buffer.count-1).each do |i|
@stack << VIM::Buffer[i].number
end
end
# Switch to the previous buffer (the one you were using before the
# current one). This is basically a smarter replacement for :b#,
# accounting for the situation where your previous buffer no longer
# exists.
def juggle_previous
buf = num_at_pos(2)
VIM::command "b #{buf}"
end
def names(n = :all)
# Get the last n buffer names by MRU. Show only as much of
# the name as necessary to differentiate between buffers of
# the same name.
cull!
names = @stack.collect { |i| VIM::bufname(i) }.reverse
if n != :all
names = names[0,n]
end
shorten_paths(names)
end
def numbers(n = :all)
# Get the last n buffer numbers by MRU.
cull!
numbers = @stack.reverse
if n == :all
numbers
else
numbers[0,n]
end
end
def num_at_pos(i)
cull!
return @stack[-i] ? @stack[-i] : @stack.first
end
def length
cull!
return @stack.length
end
def push
@stack.delete $curbuf.number
@stack << $curbuf.number
end
def pop
number = VIM::evaluate('bufnr(expand("<afile>"))')
@stack.delete number
end
private
def cull!
# Remove empty and unlisted buffers.
@stack.delete_if { |x|
not (VIM::evaluate_bool("bufexists(#{x})") and
VIM::evaluate_bool("getbufvar(#{x}, '&buflisted')"))
}
end
# NOTE: very similar to Entry::compute_buffer_entries()
def shorten_paths(buffer_names)
# Shorten each buffer name by removing all path elements which are not
# needed to differentiate a given name from other names. This usually
# results in only the basename shown, but if several buffers of the
# same basename are opened, there will be more.
# Group the buffers by common basename
common_base = Hash.new { |hash, k| hash[k] = [] }
buffer_names.each do |name|
basename = Pathname.new(name).basename.to_s
common_base[basename] << name
end
# Determine the longest common prefix for each basename group.
basename_to_prefix = {}
common_base.each do |k, names|
if names.length > 1
basename_to_prefix[k] = LustyE::longest_common_prefix(names)
end
end
# Shorten each buffer_name by removing the prefix.
buffer_names.map { |name|
base = Pathname.new(name).basename.to_s
prefix = basename_to_prefix[base]
prefix ? name[prefix.length..-1] \
: base
}
end
end
end
$lusty_buffer_explorer = LustyE::BufferExplorer.new
$lusty_filesystem_explorer = LustyE::FilesystemExplorer.new
$lusty_buffer_grep = LustyE::BufferGrep.new
$le_buffer_stack = LustyE::BufferStack.new
EOF
" vim: set sts=2 sw=2: