controller.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. # Copyright 2010-2014 Wincent Colaiuta. All rights reserved.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions are met:
  5. #
  6. # 1. Redistributions of source code must retain the above copyright notice,
  7. # this list of conditions and the following disclaimer.
  8. # 2. Redistributions in binary form must reproduce the above copyright notice,
  9. # this list of conditions and the following disclaimer in the documentation
  10. # and/or other materials provided with the distribution.
  11. #
  12. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  13. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  14. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  15. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
  16. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  17. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  18. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  19. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  20. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  21. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  22. # POSSIBILITY OF SUCH DAMAGE.
  23. require 'command-t/finder/buffer_finder'
  24. require 'command-t/finder/jump_finder'
  25. require 'command-t/finder/file_finder'
  26. require 'command-t/finder/tag_finder'
  27. require 'command-t/match_window'
  28. require 'command-t/prompt'
  29. require 'command-t/vim/path_utilities'
  30. require 'command-t/util'
  31. module CommandT
  32. class Controller
  33. include VIM::PathUtilities
  34. def initialize
  35. @prompt = Prompt.new
  36. end
  37. def show_buffer_finder
  38. @path = VIM::pwd
  39. @active_finder = buffer_finder
  40. show
  41. end
  42. def show_jump_finder
  43. @path = VIM::pwd
  44. @active_finder = jump_finder
  45. show
  46. end
  47. def show_tag_finder
  48. @path = VIM::pwd
  49. @active_finder = tag_finder
  50. show
  51. end
  52. def show_file_finder
  53. # optional parameter will be desired starting directory, or ""
  54. @path = File.expand_path(::VIM::evaluate('a:arg'), VIM::pwd)
  55. @active_finder = file_finder
  56. file_finder.path = @path
  57. show
  58. rescue Errno::ENOENT
  59. # probably a problem with the optional parameter
  60. @match_window.print_no_such_file_or_directory
  61. end
  62. def hide
  63. @match_window.close
  64. if VIM::Window.select @initial_window
  65. if @initial_buffer.number == 0
  66. # upstream bug: buffer number misreported as 0
  67. # see: https://wincent.com/issues/1617
  68. ::VIM::command "silent b #{@initial_buffer.name}"
  69. else
  70. ::VIM::command "silent b #{@initial_buffer.number}"
  71. end
  72. end
  73. end
  74. # Take current matches and stick them in the quickfix window.
  75. def quickfix
  76. hide
  77. matches = @matches.map do |match|
  78. "{ 'filename': '#{VIM::escape_for_single_quotes match}' }"
  79. end.join(', ')
  80. ::VIM::command 'call setqflist([' + matches + '])'
  81. ::VIM::command 'cope'
  82. end
  83. def refresh
  84. return unless @active_finder && @active_finder.respond_to?(:flush)
  85. @active_finder.flush
  86. list_matches!
  87. end
  88. def flush
  89. @max_height = nil
  90. @min_height = nil
  91. @file_finder = nil
  92. @tag_finder = nil
  93. end
  94. def handle_key
  95. key = ::VIM::evaluate('a:arg').to_i.chr
  96. if @focus == @prompt
  97. @prompt.add! key
  98. @needs_update = true
  99. else
  100. @match_window.find key
  101. end
  102. end
  103. def backspace
  104. if @focus == @prompt
  105. @prompt.backspace!
  106. @needs_update = true
  107. end
  108. end
  109. def delete
  110. if @focus == @prompt
  111. @prompt.delete!
  112. @needs_update = true
  113. end
  114. end
  115. def accept_selection options = {}
  116. selection = @match_window.selection
  117. hide
  118. open_selection(selection, options) unless selection.nil?
  119. end
  120. def toggle_focus
  121. @focus.unfocus # old focus
  122. @focus = @focus == @prompt ? @match_window : @prompt
  123. @focus.focus # new focus
  124. end
  125. def cancel
  126. hide
  127. end
  128. def select_next
  129. @match_window.select_next
  130. end
  131. def select_prev
  132. @match_window.select_prev
  133. end
  134. def clear
  135. @prompt.clear!
  136. list_matches!
  137. end
  138. def cursor_left
  139. @prompt.cursor_left if @focus == @prompt
  140. end
  141. def cursor_right
  142. @prompt.cursor_right if @focus == @prompt
  143. end
  144. def cursor_end
  145. @prompt.cursor_end if @focus == @prompt
  146. end
  147. def cursor_start
  148. @prompt.cursor_start if @focus == @prompt
  149. end
  150. def leave
  151. @match_window.leave
  152. end
  153. def unload
  154. @match_window.unload
  155. end
  156. def list_matches(options = {})
  157. return unless @needs_update || options[:force]
  158. @matches = @active_finder.sorted_matches_for(
  159. @prompt.abbrev,
  160. :limit => match_limit,
  161. :threads => CommandT::Util.processor_count
  162. )
  163. @match_window.matches = @matches
  164. @needs_update = false
  165. end
  166. private
  167. def list_matches!
  168. list_matches(:force => true)
  169. end
  170. def show
  171. @initial_window = $curwin
  172. @initial_buffer = $curbuf
  173. @match_window = MatchWindow.new \
  174. :highlight_color => get_string('g:CommandTHighlightColor'),
  175. :match_window_at_top => get_bool('g:CommandTMatchWindowAtTop'),
  176. :match_window_reverse => get_bool('g:CommandTMatchWindowReverse'),
  177. :min_height => min_height,
  178. :debounce_interval => get_number('g:CommandTInputDebounce', 50),
  179. :prompt => @prompt
  180. @focus = @prompt
  181. @prompt.focus
  182. register_for_key_presses
  183. set_up_autocmds
  184. clear # clears prompt and lists matches
  185. end
  186. def max_height
  187. @max_height ||= get_number('g:CommandTMaxHeight', 0)
  188. end
  189. def min_height
  190. @min_height ||= begin
  191. min_height = get_number('g:CommandTMinHeight', 0)
  192. min_height = max_height if max_height != 0 && min_height > max_height
  193. min_height
  194. end
  195. end
  196. def get_number(name, default = nil)
  197. VIM::exists?(name) ? ::VIM::evaluate("#{name}").to_i : default
  198. end
  199. def get_bool(name)
  200. VIM::exists?(name) ? ::VIM::evaluate("#{name}").to_i != 0 : nil
  201. end
  202. def get_string(name)
  203. VIM::exists?(name) ? ::VIM::evaluate("#{name}").to_s : nil
  204. end
  205. # expect a string or a list of strings
  206. def get_list_or_string name
  207. return nil unless VIM::exists?(name)
  208. list_or_string = ::VIM::evaluate("#{name}")
  209. if list_or_string.kind_of?(Array)
  210. list_or_string.map { |item| item.to_s }
  211. else
  212. list_or_string.to_s
  213. end
  214. end
  215. # Backslash-escape space, \, |, %, #, "
  216. def sanitize_path_string str
  217. # for details on escaping command-line mode arguments see: :h :
  218. # (that is, help on ":") in the Vim documentation.
  219. str.gsub(/[ \\|%#"]/, '\\\\\0')
  220. end
  221. def default_open_command
  222. if !get_bool('&hidden') && get_bool('&modified')
  223. 'sp'
  224. else
  225. 'e'
  226. end
  227. end
  228. def ensure_appropriate_window_selection
  229. # normally we try to open the selection in the current window, but there
  230. # is one exception:
  231. #
  232. # - we don't touch any "unlisted" buffer with buftype "nofile" (such as
  233. # NERDTree or MiniBufExplorer); this is to avoid things like the "Not
  234. # enough room" error which occurs when trying to open in a split in a
  235. # shallow (potentially 1-line) buffer like MiniBufExplorer is current
  236. #
  237. # Other "unlisted" buffers, such as those with buftype "help" are treated
  238. # normally.
  239. initial = $curwin
  240. while true do
  241. break unless ::VIM::evaluate('&buflisted').to_i == 0 &&
  242. ::VIM::evaluate('&buftype').to_s == 'nofile'
  243. ::VIM::command 'wincmd w' # try next window
  244. break if $curwin == initial # have already tried all
  245. end
  246. end
  247. def open_selection selection, options = {}
  248. command = options[:command] || default_open_command
  249. selection = File.expand_path selection, @path
  250. selection = relative_path_under_working_directory selection
  251. selection = sanitize_path_string selection
  252. selection = File.join('.', selection) if selection =~ /^\+/
  253. ensure_appropriate_window_selection
  254. @active_finder.open_selection command, selection, options
  255. end
  256. def map key, function, param = nil
  257. ::VIM::command "noremap <silent> <buffer> #{key} " \
  258. ":call CommandT#{function}(#{param})<CR>"
  259. end
  260. def term
  261. @term ||= ::VIM::evaluate('&term')
  262. end
  263. def register_for_key_presses
  264. # "normal" keys (interpreted literally)
  265. numbers = ('0'..'9').to_a.join
  266. lowercase = ('a'..'z').to_a.join
  267. uppercase = lowercase.upcase
  268. punctuation = '<>`@#~!"$%&/()=+*-_.,;:?\\\'{}[] ' # and space
  269. (numbers + lowercase + uppercase + punctuation).each_byte do |b|
  270. map "<Char-#{b}>", 'HandleKey', b
  271. end
  272. # "special" keys (overridable by settings)
  273. {
  274. 'AcceptSelection' => '<CR>',
  275. 'AcceptSelectionSplit' => ['<C-CR>', '<C-s>'],
  276. 'AcceptSelectionTab' => '<C-t>',
  277. 'AcceptSelectionVSplit' => '<C-v>',
  278. 'Backspace' => '<BS>',
  279. 'Cancel' => ['<C-c>', '<Esc>'],
  280. 'Clear' => '<C-u>',
  281. 'CursorEnd' => '<C-e>',
  282. 'CursorLeft' => ['<Left>', '<C-h>'],
  283. 'CursorRight' => ['<Right>', '<C-l>'],
  284. 'CursorStart' => '<C-a>',
  285. 'Delete' => '<Del>',
  286. 'Quickfix' => '<C-q>',
  287. 'Refresh' => '<C-f>',
  288. 'SelectNext' => ['<C-n>', '<C-j>', '<Down>'],
  289. 'SelectPrev' => ['<C-p>', '<C-k>', '<Up>'],
  290. 'ToggleFocus' => '<Tab>',
  291. }.each do |key, value|
  292. if override = get_list_or_string("g:CommandT#{key}Map")
  293. Array(override).each do |mapping|
  294. map mapping, key
  295. end
  296. else
  297. Array(value).each do |mapping|
  298. unless mapping == '<Esc>' && term =~ /\A(screen|xterm|vt100)/
  299. map mapping, key
  300. end
  301. end
  302. end
  303. end
  304. end
  305. def set_up_autocmds
  306. ::VIM::command 'augroup Command-T'
  307. ::VIM::command 'au!'
  308. ::VIM::command 'autocmd CursorHold <buffer> :call CommandTListMatches()'
  309. ::VIM::command 'augroup END'
  310. end
  311. # Returns the desired maximum number of matches, based on available
  312. # vertical space and the g:CommandTMaxHeight option.
  313. def match_limit
  314. limit = VIM::Screen.lines - 5
  315. limit = 1 if limit < 0
  316. limit = [limit, max_height].min if max_height > 0
  317. limit
  318. end
  319. def buffer_finder
  320. @buffer_finder ||= CommandT::BufferFinder.new
  321. end
  322. def file_finder
  323. @file_finder ||= CommandT::FileFinder.new nil,
  324. :max_depth => get_number('g:CommandTMaxDepth'),
  325. :max_files => get_number('g:CommandTMaxFiles'),
  326. :max_caches => get_number('g:CommandTMaxCachedDirectories'),
  327. :always_show_dot_files => get_bool('g:CommandTAlwaysShowDotFiles'),
  328. :never_show_dot_files => get_bool('g:CommandTNeverShowDotFiles'),
  329. :scan_dot_directories => get_bool('g:CommandTScanDotDirectories'),
  330. :wild_ignore => get_string('g:CommandTWildIgnore'),
  331. :scanner => get_string('g:CommandTFileScanner')
  332. end
  333. def jump_finder
  334. @jump_finder ||= CommandT::JumpFinder.new
  335. end
  336. def tag_finder
  337. @tag_finder ||= CommandT::TagFinder.new \
  338. :include_filenames => get_bool('g:CommandTTagIncludeFilenames')
  339. end
  340. end # class Controller
  341. end # module commandT