" Copyright: Copyright (C) 2007-2010 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 " Matt Tolton " 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 " " Release Date: June 28, 2010 " Version: 3.1 " " Usage: " lf - Opens the filesystem explorer. " lr - Opens the filesystem explorer from the " directory of the current file. " lb - Opens the buffer explorer. " lg - Opens the buffer grep. " " You can also use the commands: " " ":LustyFilesystemExplorer" " ":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: " " open selected match " open selected match " cancel " cancel " cancel " " open selected match in a new [t]ab " open selected match in a new h[o]rizontal split " open selected match in a new [v]ertical split " " select [n]ext match " select [p]revious match " " clear prompt " " Additional shortcuts for the filesystem explorer: " " ascend one directory at prompt " [r]efresh directory contents " open [a]ll files in current table " create new buffer with the given name and path " " Filesystem Explorer: " " - Directory contents are memoized. ( 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 currently open 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 " . " " " 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 " " 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? " - abbrev "a" should score e.g. "ad" higher than "m-a" " 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 LustyBufferExplorerStart() command LustyFilesystemExplorer :call LustyFilesystemExplorerStart() command LustyFilesystemExplorerFromHere :call LustyFilesystemExplorerFromHereStart() command LustyBufferGrep :call LustyBufferGrepStart() " Deprecated command names. command BufferExplorer :call \ deprecated('BufferExplorer', 'LustyBufferExplorer') command FilesystemExplorer :call \ deprecated('FilesystemExplorer', 'LustyFilesystemExplorer') command FilesystemExplorerFromHere :call \ 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 lf :LustyFilesystemExplorer nmap lr :LustyFilesystemExplorerFromHere nmap lb :LustyBufferExplorer nmap lg :LustyBufferGrep " Vim-to-ruby function calls. function! s:LustyFilesystemExplorerStart() ruby LustyE::profile() { $lusty_filesystem_explorer.run_from_wd } endfunction function! s:LustyFilesystemExplorerFromHereStart() ruby LustyE::profile() { $lusty_filesystem_explorer.run_from_here } 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.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}))") "" 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, and double quotes. 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 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 LustyE::assert(false, "couldn't find buffer #{n}") 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 # 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 # Port of Ryan McGeary's LiquidMetal fuzzy matching algorithm found at: # http://github.com/rmm5t/liquidmetal/tree/master. module LiquidMetal @@SCORE_NO_MATCH = 0.0 @@SCORE_MATCH = 1.0 @@SCORE_TRAILING = 0.8 @@SCORE_TRAILING_BUT_STARTED = 0.90 @@SCORE_BUFFER = 0.85 def self.score(string, abbrev) return @@SCORE_TRAILING if abbrev.empty? return @@SCORE_NO_MATCH if abbrev.length > string.length scores = buildScoreArray(string, abbrev) # Faster than Array#inject... sum = 0.0 scores.each { |x| sum += x } return sum / scores.length; end def self.buildScoreArray(string, abbrev) scores = Array.new(string.length) lower = string.downcase() lastIndex = 0 started = false abbrev.downcase().each_char do |c| index = lower.index(c, lastIndex) return scores.fill(@@SCORE_NO_MATCH) if index.nil? started = true if index == 0 if index > 0 and " ._-".include?(string[index - 1]) scores[index - 1] = @@SCORE_MATCH scores.fill(@@SCORE_BUFFER, lastIndex...(index - 1)) elsif string[index] >= ?A and string[index] <= ?Z scores.fill(@@SCORE_BUFFER, lastIndex...index) else scores.fill(@@SCORE_NO_MATCH, lastIndex...index) end scores[index] = @@SCORE_MATCH lastIndex = index + 1 end trailing_score = started ? @@SCORE_TRAILING_BUT_STARTED : @@SCORE_TRAILING scores.fill(trailing_score, lastIndex) return scores 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 def self.compute_buffer_entries() buffer_entries = [] $le_buffer_stack.numbers.each do |n| o = VIM::Buffer.obj_for_bufnr(n) 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 = \ (@selected_index + 1) % @current_sorted_matches.size refresh_mode = :no_recompute when 16 # C-p (select previous) @selected_index = \ (@selected_index - 1) % @current_sorted_matches.size 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? @display.print @current_sorted_matches.map { |x| x.label } @prompt.print 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 = LiquidMetal.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_here return if @running start_path = if $curbuf.name.nil? VIM::getcwd() else VIM::evaluate("expand('%:p:h')") end @prompt.set!(start_path + File::SEPARATOR) run() end def run_from_wd return if @running @prompt.set!(VIM::getcwd() + File::SEPARATOR) run() end def key_pressed() i = VIM::evaluate("a:code_arg").to_i case i when 1, 10 # , 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 # 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 # 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 = LiquidMetal.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 VIM::pretty_msg("Comment", @@PROMPT, "None", VIM::single_quote_escape(@input), "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 $. 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") 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}" 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' end # # Key mappings - we need to reroute user input. # # Non-special printable characters. printables = '/!"#$%&\'()*+,-.0123456789:<=>?#@"' \ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '[]^_`abcdefghijklmnopqrstuvwxyz{}~' map = "noremap " printables.each_byte do |b| VIM::command "#{map} :call #{prefix}KeyPressed(#{b})" end # Special characters VIM::command "#{map} :call #{prefix}KeyPressed(9)" VIM::command "#{map} :call #{prefix}KeyPressed(92)" VIM::command "#{map} :call #{prefix}KeyPressed(32)" VIM::command "#{map} \026| :call #{prefix}KeyPressed(124)" VIM::command "#{map} :call #{prefix}KeyPressed(8)" VIM::command "#{map} :call #{prefix}KeyPressed(8)" VIM::command "#{map} :call #{prefix}KeyPressed(8)" VIM::command "#{map} :call #{prefix}KeyPressed(13)" VIM::command "#{map} :call #{prefix}KeyPressed(10)" VIM::command "#{map} :call #{prefix}KeyPressed(1)" VIM::command "#{map} :call #{prefix}Cancel()" VIM::command "#{map} :call #{prefix}Cancel()" VIM::command "#{map} :call #{prefix}Cancel()" VIM::command "#{map} :call #{prefix}KeyPressed(23)" VIM::command "#{map} :call #{prefix}KeyPressed(14)" VIM::command "#{map} :call #{prefix}KeyPressed(16)" VIM::command "#{map} :call #{prefix}KeyPressed(15)" VIM::command "#{map} :call #{prefix}KeyPressed(20)" VIM::command "#{map} :call #{prefix}KeyPressed(22)" VIM::command "#{map} :call #{prefix}KeyPressed(5)" VIM::command "#{map} :call #{prefix}KeyPressed(18)" VIM::command "#{map} :call #{prefix}KeyPressed(21)" 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 - string.length)) rows[i] << @@COLUMN_SEPARATOR end end col_index += 1 break if col_index >= col_count end print_rows(rows, truncated) 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| column_width = column.max { |a, b| a.length <=> b.length }.length 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(""))') @stack.delete number end private def cull! # Remove empty buffers. @stack.delete_if { |x| not VIM::evaluate_bool("bufexists(#{x})") } 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: