# Copyright 2010-2012 Wincent Colaiuta. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. require 'ostruct' require 'command-t/settings' module CommandT class MatchWindow SELECTION_MARKER = '> ' MARKER_LENGTH = SELECTION_MARKER.length UNSELECTED_MARKER = ' ' * MARKER_LENGTH MH_START = '' MH_END = '' @@buffer = nil def initialize options = {} @prompt = options[:prompt] @reverse_list = options[:match_window_reverse] @min_height = options[:min_height] # save existing window dimensions so we can restore them later @windows = [] (0..(::VIM::Window.count - 1)).each do |i| @windows << OpenStruct.new(:index => i, :height => ::VIM::Window[i].height, :width => ::VIM::Window[i].width) end # global settings (must manually save and restore) @settings = Settings.new ::VIM::set_option 'timeout' # ensure mappings timeout ::VIM::set_option 'timeoutlen=0' # respond immediately to mappings ::VIM::set_option 'nohlsearch' # don't highlight search strings ::VIM::set_option 'noinsertmode' # don't make Insert mode the default ::VIM::set_option 'noshowcmd' # don't show command info on last line ::VIM::set_option 'report=9999' # don't show "X lines changed" reports ::VIM::set_option 'sidescroll=0' # don't sidescroll in jumps ::VIM::set_option 'sidescrolloff=0' # don't sidescroll automatically ::VIM::set_option 'noequalalways' # don't auto-balance window sizes # show match window split_location = options[:match_window_at_top] ? 'topleft' : 'botright' if @@buffer # still have buffer from last time ::VIM::command "silent! #{split_location} #{@@buffer.number}sbuffer" raise "Can't re-open GoToFile buffer" unless $curbuf.number == @@buffer.number $curwin.height = 1 else # creating match window for first time and set it up split_command = "silent! #{split_location} 1split GoToFile" [ split_command, 'setlocal bufhidden=unload', # unload buf when no longer displayed 'setlocal buftype=nofile', # buffer is not related to any file 'setlocal nomodifiable', # prevent manual edits 'setlocal noswapfile', # don't create a swapfile 'setlocal nowrap', # don't soft-wrap 'setlocal nonumber', # don't show line numbers 'setlocal nolist', # don't use List mode (visible tabs etc) 'setlocal foldcolumn=0', # don't show a fold column at side 'setlocal foldlevel=99', # don't fold anything 'setlocal nocursorline', # don't highlight line cursor is on 'setlocal nospell', # spell-checking off 'setlocal nobuflisted', # don't show up in the buffer list 'setlocal textwidth=0' # don't hard-wrap (break long lines) ].each { |command| ::VIM::command command } # don't show the color column ::VIM::command 'setlocal colorcolumn=0' if VIM::exists?('+colorcolumn') # don't show relative line numbers ::VIM::command 'setlocal norelativenumber' if VIM::exists?('+relativenumber') # sanity check: make sure the buffer really was created raise "Can't find GoToFile buffer" unless $curbuf.name.match /GoToFile\z/ @@buffer = $curbuf end # syntax coloring if VIM::has_syntax? ::VIM::command "syntax match CommandTSelection \"^#{SELECTION_MARKER}.\\+$\"" ::VIM::command 'syntax match CommandTNoEntries "^-- NO MATCHES --$"' ::VIM::command 'syntax match CommandTNoEntries "^-- NO SUCH FILE OR DIRECTORY --$"' ::VIM::command 'setlocal synmaxcol=9999' if VIM::has_conceal? ::VIM::command 'setlocal conceallevel=2' ::VIM::command 'setlocal concealcursor=nvic' ::VIM::command 'syntax region CommandTCharMatched ' \ "matchgroup=CommandTCharMatched start=+#{MH_START}+ " \ "matchgroup=CommandTCharMatchedEnd end=+#{MH_END}+ concealends" ::VIM::command 'highlight def CommandTCharMatched ' \ 'term=bold,underline cterm=bold,underline ' \ 'gui=bold,underline' end ::VIM::command 'highlight link CommandTSelection Visual' ::VIM::command 'highlight link CommandTNoEntries Error' ::VIM::evaluate 'clearmatches()' # hide cursor @cursor_highlight = get_cursor_highlight hide_cursor end # perform cleanup using an autocmd to ensure we don't get caught out # by some unexpected means of dismissing or leaving the Command-T window # (eg. , etc) ::VIM::command 'autocmd! * ' ::VIM::command 'autocmd BufLeave silent! ruby $command_t.leave' ::VIM::command 'autocmd BufUnload silent! ruby $command_t.unload' @has_focus = false @selection = nil @abbrev = '' @window = $curwin end def close # Unlisted buffers like those provided by Netrw, NERDTree and Vim's help # don't actually appear in the buffer list; if they are the only such # buffers present when Command-T is invoked (for example, when invoked # immediately after starting Vim with a directory argument, like `vim .`) # then performing the normal clean-up will yield an "E90: Cannot unload # last buffer" error. We can work around that by doing a :quit first. if ::VIM::Buffer.count == 0 ::VIM::command 'silent quit' end # Workaround for upstream bug in Vim 7.3 on some platforms # # On some platforms, $curbuf.number always returns 0. One workaround is # to build Vim with --disable-largefile, but as this is producing lots of # support requests, implement the following fallback to the buffer name # instead, at least until upstream gets fixed. # # For more details, see: https://wincent.com/issues/1617 if $curbuf.number == 0 # use bwipeout as bunload fails if passed the name of a hidden buffer ::VIM::command 'silent! bwipeout! GoToFile' @@buffer = nil else ::VIM::command "silent! bunload! #{@@buffer.number}" end end def leave close unload end def unload restore_window_dimensions @settings.restore @prompt.dispose show_cursor end def add! char @abbrev += char end def backspace! @abbrev.chop! end def select_next if @selection < @matches.length - 1 @selection += 1 print_match(@selection - 1) # redraw old selection (removes marker) print_match(@selection) # redraw new selection (adds marker) move_cursor_to_selected_line else # (possibly) loop or scroll end end def select_prev if @selection > 0 @selection -= 1 print_match(@selection + 1) # redraw old selection (removes marker) print_match(@selection) # redraw new selection (adds marker) move_cursor_to_selected_line else # (possibly) loop or scroll end end def matches= matches matches = matches.reverse if @reverse_list if matches != @matches @matches = matches @selection = @reverse_list ? @matches.length - 1 : 0 print_matches move_cursor_to_selected_line end end def focus unless @has_focus @has_focus = true if VIM::has_syntax? ::VIM::command 'highlight link CommandTSelection Search' end end end def unfocus if @has_focus @has_focus = false if VIM::has_syntax? ::VIM::command 'highlight link CommandTSelection Visual' end end end def find char # is this a new search or the continuation of a previous one? now = Time.now if @last_key_time.nil? or @last_key_time < (now - 0.5) @find_string = char else @find_string += char end @last_key_time = now # see if there's anything up ahead that matches @matches.each_with_index do |match, idx| if match[0, @find_string.length].casecmp(@find_string) == 0 old_selection = @selection @selection = idx print_match(old_selection) # redraw old selection (removes marker) print_match(@selection) # redraw new selection (adds marker) break end end end # Returns the currently selected item as a String. def selection @matches[@selection] end def print_no_such_file_or_directory print_error 'NO SUCH FILE OR DIRECTORY' end private def move_cursor_to_selected_line # on some non-GUI terminals, the cursor doesn't hide properly # so we move the cursor to prevent it from blinking away in the # upper-left corner in a distracting fashion @window.cursor = [@selection + 1, 0] end def print_error msg return unless VIM::Window.select(@window) unlock clear @window.height = @min_height > 0 ? @min_height : 1 @@buffer[1] = "-- #{msg} --" lock end def restore_window_dimensions # sort from tallest to shortest, tie-breaking on window width @windows.sort! do |a, b| order = b.height <=> a.height if order.zero? b.width <=> a.width else order end end # starting with the tallest ensures that there are no constraints # preventing windows on the side of vertical splits from regaining # their original full size @windows.each do |w| # beware: window may be nil if window = ::VIM::Window[w.index] window.height = w.height window.width = w.width end end end def match_text_for_idx idx match = truncated_match @matches[idx].to_s if idx == @selection prefix = SELECTION_MARKER suffix = padding_for_selected_match match else if VIM::has_syntax? && VIM::has_conceal? match = match_with_syntax_highlight match end prefix = UNSELECTED_MARKER suffix = '' end prefix + match + suffix end # Highlight matching characters within the matched string. # # Note that this is only approximate; it will highlight the first matching # instances within the string, which may not actually be the instances that # were used by the matching/scoring algorithm to determine the best score # for the match. # def match_with_syntax_highlight match highlight_chars = @prompt.abbrev.downcase.chars.to_a match.chars.inject([]) do |output, char| if char.downcase == highlight_chars.first highlight_chars.shift output.concat [MH_START, char, MH_END] else output << char end end.join end # Print just the specified match. def print_match idx return unless VIM::Window.select(@window) unlock @@buffer[idx + 1] = match_text_for_idx idx lock end # Print all matches. def print_matches match_count = @matches.length if match_count == 0 print_error 'NO MATCHES' else return unless VIM::Window.select(@window) unlock clear actual_lines = 1 @window_width = @window.width # update cached value max_lines = VIM::Screen.lines - 5 max_lines = 1 if max_lines < 0 actual_lines = match_count < @min_height ? @min_height : match_count actual_lines = max_lines if actual_lines > max_lines @window.height = actual_lines (1..actual_lines).each do |line| idx = line - 1 if @@buffer.count >= line @@buffer[line] = match_text_for_idx idx else @@buffer.append line - 1, match_text_for_idx(idx) end end lock end end # Prepare padding for match text (trailing spaces) so that selection # highlighting extends all the way to the right edge of the window. def padding_for_selected_match str len = str.length if len >= @window_width - MARKER_LENGTH '' else ' ' * (@window_width - MARKER_LENGTH - len) end end # Convert "really/long/path" into "really...path" based on available # window width. def truncated_match str len = str.length available_width = @window_width - MARKER_LENGTH return str if len <= available_width left = (available_width / 2) - 1 right = (available_width / 2) - 2 + (available_width % 2) str[0, left] + '...' + str[-right, right] end def clear # range = % (whole buffer) # action = d (delete) # register = _ (black hole register, don't record deleted text) ::VIM::command 'silent %d _' end def get_cursor_highlight # there are 3 possible formats to check for, each needing to be # transformed in a certain way in order to reapply the highlight: # Cursor xxx guifg=bg guibg=fg -> :hi! Cursor guifg=bg guibg=fg # Cursor xxx links to SomethingElse -> :hi! link Cursor SomethingElse # Cursor xxx cleared -> :hi! clear Cursor highlight = VIM::capture 'silent! 0verbose highlight Cursor' if highlight =~ /^Cursor\s+xxx\s+links to (\w+)/ "link Cursor #{$~[1]}" elsif highlight =~ /^Cursor\s+xxx\s+cleared/ 'clear Cursor' elsif highlight =~ /Cursor\s+xxx\s+(.+)/ "Cursor #{$~[1]}" else # likely cause E411 Cursor highlight group not found nil end end def hide_cursor if @cursor_highlight ::VIM::command 'highlight Cursor NONE' end end def show_cursor if @cursor_highlight ::VIM::command "highlight #{@cursor_highlight}" end end def lock ::VIM::command 'setlocal nomodifiable' end def unlock ::VIM::command 'setlocal modifiable' end end end