# Copyright 2010-2014 Greg Hurrell. All rights reserved. # Licensed under the terms of the BSD 2-clause license. require 'command-t/finder/buffer_finder' require 'command-t/finder/jump_finder' require 'command-t/finder/file_finder' require 'command-t/finder/mru_buffer_finder' require 'command-t/finder/tag_finder' require 'command-t/match_window' require 'command-t/prompt' require 'command-t/vim/path_utilities' require 'command-t/util' module CommandT class Controller include VIM::PathUtilities def initialize @prompt = Prompt.new end def show_buffer_finder @path = VIM::pwd @active_finder = buffer_finder show end def show_jump_finder @path = VIM::pwd @active_finder = jump_finder show end def show_mru_finder @path = VIM::pwd @active_finder = mru_finder show end def show_tag_finder @path = VIM::pwd @active_finder = tag_finder show end def show_file_finder # optional parameter will be desired starting directory, or "" arg = ::VIM::evaluate('a:arg') if arg && arg.size > 0 @path = File.expand_path(arg, VIM::pwd) else traverse = VIM::get_string('g:CommandTTraverseSCM') || 'file' case traverse when 'file' @path = nearest_ancestor(VIM::current_file_dir, scm_markers) when 'dir' @path = nearest_ancestor(VIM::pwd, scm_markers) end @path = VIM::pwd unless @path end @active_finder = file_finder file_finder.path = @path show rescue Errno::ENOENT # probably a problem with the optional parameter @match_window.print_no_such_file_or_directory end def hide @match_window.leave if VIM::Window.select @initial_window if @initial_buffer.number == 0 # upstream bug: buffer number misreported as 0 # see: https://wincent.com/issues/1617 ::VIM::command "silent b #{@initial_buffer.name}" else ::VIM::command "silent b #{@initial_buffer.number}" end end end # Take current matches and stick them in the quickfix window. def quickfix hide matches = @matches.map do |match| "{ 'filename': '#{VIM::escape_for_single_quotes match}' }" end.join(', ') ::VIM::command 'call setqflist([' + matches + '])' ::VIM::command 'cope' end def refresh return unless @active_finder && @active_finder.respond_to?(:flush) @active_finder.flush list_matches! end def flush @max_height = nil @min_height = nil @file_finder = nil @tag_finder = nil end def handle_key key = ::VIM::evaluate('a:arg').to_i.chr if @focus == @prompt @prompt.add! key @needs_update = true else @match_window.find key end end def backspace if @focus == @prompt @prompt.backspace! @needs_update = true end end def delete if @focus == @prompt @prompt.delete! @needs_update = true end end def accept_selection(options = {}) selection = @match_window.selection hide open_selection(selection, options) unless selection.nil? end def toggle_focus @focus.unfocus # old focus @focus = @focus == @prompt ? @match_window : @prompt @focus.focus # new focus end def cancel hide end def select_next @match_window.select_next end def select_prev @match_window.select_prev end def clear @prompt.clear! list_matches! end def clear_prev_word @prompt.clear_prev_word! list_matches! end def cursor_left @prompt.cursor_left if @focus == @prompt end def cursor_right @prompt.cursor_right if @focus == @prompt end def cursor_end @prompt.cursor_end if @focus == @prompt end def cursor_start @prompt.cursor_start if @focus == @prompt end def leave @match_window.leave end def unload @match_window.unload end def list_matches(options = {}) return unless @needs_update || options[:force] @matches = @active_finder.sorted_matches_for( @prompt.abbrev, :case_sensitive => case_sensitive?, :limit => match_limit, :threads => CommandT::Util.processor_count ) @match_window.matches = @matches @needs_update = false end def tab_command VIM::get_string('g:CommandTAcceptSelectionTabCommand') || 'tabe' end def split_command VIM::get_string('g:CommandTAcceptSelectionSplitCommand') || 'sp' end def vsplit_command VIM::get_string('g:CommandTAcceptSelectionVSplitCommand') || 'vs' end private def scm_markers markers = VIM::get_string('g:CommandTSCMDirectories') markers = markers && markers.split(/\s*,\s*/) markers = %w[.git .hg .svn .bzr _darcs] unless markers && markers.length markers end def list_matches! list_matches(:force => true) end def show @initial_window = $curwin @initial_buffer = $curbuf @match_window = MatchWindow.new \ :highlight_color => VIM::get_string('g:CommandTHighlightColor'), :match_window_at_top => VIM::get_bool('g:CommandTMatchWindowAtTop'), :match_window_reverse => VIM::get_bool('g:CommandTMatchWindowReverse'), :min_height => min_height, :debounce_interval => VIM::get_number('g:CommandTInputDebounce') || 50, :prompt => @prompt @focus = @prompt @prompt.focus register_for_key_presses set_up_autocmds clear # clears prompt and lists matches end def max_height @max_height ||= VIM::get_number('g:CommandTMaxHeight') || 0 end def min_height @min_height ||= begin min_height = VIM::get_number('g:CommandTMinHeight') || 0 min_height = max_height if max_height != 0 && min_height > max_height min_height end end def case_sensitive? if @prompt.abbrev.match(/[A-Z]/) if VIM::exists?('g:CommandTSmartCase') smart_case = VIM::get_bool('g:CommandTSmartCase') else smart_case = VIM::get_bool('&smartcase') end if smart_case return true end end if VIM::exists?('g:CommandTIgnoreCase') return !VIM::get_bool('g:CommandTIgnoreCase') end false end # Backslash-escape space, \, |, %, #, " def sanitize_path_string(str) # for details on escaping command-line mode arguments see: :h : # (that is, help on ":") in the Vim documentation. str.gsub(/[ \\|%#"]/, '\\\\\0') end def default_open_command if !VIM::get_bool('&modified') || VIM::get_bool('&hidden') || VIM::get_bool('&autowriteall') && !VIM::get_bool('&readonly') VIM::get_string('g:CommandTAcceptSelectionCommand') || 'e' else 'sp' end end def ensure_appropriate_window_selection # normally we try to open the selection in the current window, but there # is one exception: # # - we don't touch any "unlisted" buffer with buftype "nofile" (such as # NERDTree or MiniBufExplorer); this is to avoid things like the "Not # enough room" error which occurs when trying to open in a split in a # shallow (potentially 1-line) buffer like MiniBufExplorer is current # # Other "unlisted" buffers, such as those with buftype "help" are treated # normally. initial = $curwin while true do break unless ::VIM::evaluate('&buflisted').to_i == 0 && ::VIM::evaluate('&buftype').to_s == 'nofile' ::VIM::command 'wincmd w' # try next window break if $curwin == initial # have already tried all end end def open_selection(selection, options = {}) command = options[:command] || default_open_command selection = File.expand_path selection, @path selection = relative_path_under_working_directory selection selection = sanitize_path_string selection selection = File.join('.', selection) if selection =~ /^\+/ ensure_appropriate_window_selection @active_finder.open_selection command, selection, options end def map(key, function, param = nil) ::VIM::command "noremap #{key} " \ ":call CommandT#{function}(#{param})" end def term @term ||= ::VIM::evaluate('&term') end def register_for_key_presses # "normal" keys (interpreted literally) numbers = ('0'..'9').to_a.join lowercase = ('a'..'z').to_a.join uppercase = lowercase.upcase punctuation = '<>`@#~!"$%&/()=+*-_.,;:?\\\'{}[] ' # and space (numbers + lowercase + uppercase + punctuation).each_byte do |b| map "", 'HandleKey', b end # "special" keys (overridable by settings) { 'AcceptSelection' => '', 'AcceptSelectionSplit' => ['', ''], 'AcceptSelectionTab' => '', 'AcceptSelectionVSplit' => '', 'Backspace' => '', 'Cancel' => ['', ''], 'Clear' => '', 'ClearPrevWord' => '', 'CursorEnd' => '', 'CursorLeft' => ['', ''], 'CursorRight' => ['', ''], 'CursorStart' => '', 'Delete' => '', 'Quickfix' => '', 'Refresh' => '', 'SelectNext' => ['', '', ''], 'SelectPrev' => ['', '', ''], 'ToggleFocus' => '', }.each do |key, value| if override = VIM::get_list_or_string("g:CommandT#{key}Map") Array(override).each do |mapping| map mapping, key end else Array(value).each do |mapping| unless mapping == '' && term =~ /\A(screen|xterm|vt100)/ map mapping, key end end end end end def set_up_autocmds ::VIM::command 'augroup Command-T' ::VIM::command 'au!' ::VIM::command 'autocmd CursorHold :call CommandTListMatches()' ::VIM::command 'augroup END' end # Returns the desired maximum number of matches, based on available vertical # space and the g:CommandTMaxHeight option. # # Note the "available" space is actually a theoretical upper bound; it takes # into account screen dimensions but not things like existing splits which # may reduce the amount of space in practice. def match_limit limit = [1, VIM::Screen.lines - 5].max limit = [limit, max_height].min if max_height > 0 limit end def buffer_finder @buffer_finder ||= CommandT::BufferFinder.new end def mru_finder @mru_finder ||= CommandT::MRUBufferFinder.new end def file_finder @file_finder ||= CommandT::FileFinder.new nil, :max_depth => VIM::get_number('g:CommandTMaxDepth'), :max_files => VIM::get_number('g:CommandTMaxFiles'), :max_caches => VIM::get_number('g:CommandTMaxCachedDirectories'), :always_show_dot_files => VIM::get_bool('g:CommandTAlwaysShowDotFiles'), :never_show_dot_files => VIM::get_bool('g:CommandTNeverShowDotFiles'), :scan_dot_directories => VIM::get_bool('g:CommandTScanDotDirectories'), :wild_ignore => VIM::get_string('g:CommandTWildIgnore'), :scanner => VIM::get_string('g:CommandTFileScanner') end def jump_finder @jump_finder ||= CommandT::JumpFinder.new end def tag_finder @tag_finder ||= CommandT::TagFinder.new \ :include_filenames => VIM::get_bool('g:CommandTTagIncludeFilenames') end end # class Controller end # module CommandT