1
0

match_window.rb 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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 'ostruct'
  24. require 'command-t/settings'
  25. module CommandT
  26. class MatchWindow
  27. SELECTION_MARKER = '> '
  28. MARKER_LENGTH = SELECTION_MARKER.length
  29. UNSELECTED_MARKER = ' ' * MARKER_LENGTH
  30. MH_START = '<commandt>'
  31. MH_END = '</commandt>'
  32. @@buffer = nil
  33. def initialize options = {}
  34. @highlight_color = options[:highlight_color] || 'PmenuSel'
  35. @min_height = options[:min_height]
  36. @prompt = options[:prompt]
  37. @reverse_list = options[:match_window_reverse]
  38. # save existing window dimensions so we can restore them later
  39. @windows = []
  40. (0..(::VIM::Window.count - 1)).each do |i|
  41. @windows << OpenStruct.new(:index => i,
  42. :height => ::VIM::Window[i].height,
  43. :width => ::VIM::Window[i].width)
  44. end
  45. set 'timeout', true # ensure mappings timeout
  46. set 'hlsearch', false # don't highlight search strings
  47. set 'insertmode', false # don't make Insert mode the default
  48. set 'showcmd', false # don't show command info on last line
  49. set 'equalalways', false # don't auto-balance window sizes
  50. set 'timeoutlen', 0 # respond immediately to mappings
  51. set 'report', 9999 # don't show "X lines changed" reports
  52. set 'sidescroll', 0 # don't sidescroll in jumps
  53. set 'sidescrolloff', 0 # don't sidescroll automatically
  54. set 'updatetime', options[:debounce_interval]
  55. # show match window
  56. split_location = options[:match_window_at_top] ? 'topleft' : 'botright'
  57. if @@buffer # still have buffer from last time
  58. ::VIM::command "silent! #{split_location} #{@@buffer.number}sbuffer"
  59. raise "Can't re-open GoToFile buffer" unless $curbuf.number == @@buffer.number
  60. $curwin.height = 1
  61. else # creating match window for first time and set it up
  62. ::VIM::command "silent! #{split_location} 1split GoToFile"
  63. set 'bufhidden', 'unload' # unload buf when no longer displayed
  64. set 'buftype', 'nofile' # buffer is not related to any file
  65. set 'modifiable', false # prevent manual edits
  66. set 'swapfile', false # don't create a swapfile
  67. set 'wrap', false # don't soft-wrap
  68. set 'number', false # don't show line numbers
  69. set 'list', false # don't use List mode (visible tabs etc)
  70. set 'foldcolumn', 0 # don't show a fold column at side
  71. set 'foldlevel', 99 # don't fold anything
  72. set 'cursorline', false # don't highlight line cursor is on
  73. set 'spell', false # spell-checking off
  74. set 'buflisted', false # don't show up in the buffer list
  75. set 'textwidth', 0 # don't hard-wrap (break long lines)
  76. # don't show the color column
  77. set 'colorcolumn', 0 if VIM::exists?('+colorcolumn')
  78. # don't show relative line numbers
  79. set 'relativenumber', false if VIM::exists?('+relativenumber')
  80. # sanity check: make sure the buffer really was created
  81. raise "Can't find GoToFile buffer" unless $curbuf.name.match /GoToFile\z/
  82. @@buffer = $curbuf
  83. end
  84. # syntax coloring
  85. if VIM::has_syntax?
  86. ::VIM::command "syntax match CommandTSelection \"^#{SELECTION_MARKER}.\\+$\""
  87. ::VIM::command 'syntax match CommandTNoEntries "^-- NO MATCHES --$"'
  88. ::VIM::command 'syntax match CommandTNoEntries "^-- NO SUCH FILE OR DIRECTORY --$"'
  89. set 'synmaxcol', 9999
  90. if VIM::has_conceal?
  91. set 'conceallevel', 2
  92. set 'concealcursor', 'nvic'
  93. ::VIM::command 'syntax region CommandTCharMatched ' \
  94. "matchgroup=CommandTCharMatched start=+#{MH_START}+ " \
  95. "matchgroup=CommandTCharMatchedEnd end=+#{MH_END}+ concealends"
  96. ::VIM::command 'highlight def CommandTCharMatched ' \
  97. 'term=bold,underline cterm=bold,underline ' \
  98. 'gui=bold,underline'
  99. end
  100. ::VIM::command "highlight link CommandTSelection #{@highlight_color}"
  101. ::VIM::command 'highlight link CommandTNoEntries Error'
  102. # hide cursor
  103. @cursor_highlight = get_cursor_highlight
  104. hide_cursor
  105. end
  106. # perform cleanup using an autocmd to ensure we don't get caught out
  107. # by some unexpected means of dismissing or leaving the Command-T window
  108. # (eg. <C-W q>, <C-W k> etc)
  109. ::VIM::command 'autocmd! * <buffer>'
  110. ::VIM::command 'autocmd BufLeave <buffer> silent! ruby $command_t.leave'
  111. ::VIM::command 'autocmd BufUnload <buffer> silent! ruby $command_t.unload'
  112. @has_focus = false
  113. @selection = nil
  114. @abbrev = ''
  115. @window = $curwin
  116. end
  117. def close
  118. # Unlisted buffers like those provided by Netrw, NERDTree and Vim's help
  119. # don't actually appear in the buffer list; if they are the only such
  120. # buffers present when Command-T is invoked (for example, when invoked
  121. # immediately after starting Vim with a directory argument, like `vim .`)
  122. # then performing the normal clean-up will yield an "E90: Cannot unload
  123. # last buffer" error. We can work around that by doing a :quit first.
  124. if ::VIM::Buffer.count == 0
  125. ::VIM::command 'silent quit'
  126. end
  127. # Workaround for upstream bug in Vim 7.3 on some platforms
  128. #
  129. # On some platforms, $curbuf.number always returns 0. One workaround is
  130. # to build Vim with --disable-largefile, but as this is producing lots of
  131. # support requests, implement the following fallback to the buffer name
  132. # instead, at least until upstream gets fixed.
  133. #
  134. # For more details, see: https://wincent.com/issues/1617
  135. if $curbuf.number == 0
  136. # use bwipeout as bunload fails if passed the name of a hidden buffer
  137. ::VIM::command 'silent! bwipeout! GoToFile'
  138. @@buffer = nil
  139. else
  140. ::VIM::command "silent! bunload! #{@@buffer.number}"
  141. end
  142. end
  143. def leave
  144. close
  145. unload
  146. end
  147. def unload
  148. restore_window_dimensions
  149. @settings.restore
  150. @prompt.dispose
  151. show_cursor
  152. end
  153. def add! char
  154. @abbrev += char
  155. end
  156. def backspace!
  157. @abbrev.chop!
  158. end
  159. def select_next
  160. if @selection < @matches.length - 1
  161. @selection += 1
  162. print_match(@selection - 1) # redraw old selection (removes marker)
  163. print_match(@selection) # redraw new selection (adds marker)
  164. move_cursor_to_selected_line
  165. else
  166. # (possibly) loop or scroll
  167. end
  168. end
  169. def select_prev
  170. if @selection > 0
  171. @selection -= 1
  172. print_match(@selection + 1) # redraw old selection (removes marker)
  173. print_match(@selection) # redraw new selection (adds marker)
  174. move_cursor_to_selected_line
  175. else
  176. # (possibly) loop or scroll
  177. end
  178. end
  179. def matches= matches
  180. matches = matches.reverse if @reverse_list
  181. if matches != @matches
  182. @matches = matches
  183. @selection = @reverse_list ? @matches.length - 1 : 0
  184. print_matches
  185. move_cursor_to_selected_line
  186. end
  187. end
  188. def focus
  189. unless @has_focus
  190. @has_focus = true
  191. if VIM::has_syntax?
  192. ::VIM::command 'highlight link CommandTSelection Search'
  193. end
  194. end
  195. end
  196. def unfocus
  197. if @has_focus
  198. @has_focus = false
  199. if VIM::has_syntax?
  200. ::VIM::command "highlight link CommandTSelection #{@highlight_color}"
  201. end
  202. end
  203. end
  204. def find char
  205. # is this a new search or the continuation of a previous one?
  206. now = Time.now
  207. if @last_key_time.nil? or @last_key_time < (now - 0.5)
  208. @find_string = char
  209. else
  210. @find_string += char
  211. end
  212. @last_key_time = now
  213. # see if there's anything up ahead that matches
  214. @matches.each_with_index do |match, idx|
  215. if match[0, @find_string.length].casecmp(@find_string) == 0
  216. old_selection = @selection
  217. @selection = idx
  218. print_match(old_selection) # redraw old selection (removes marker)
  219. print_match(@selection) # redraw new selection (adds marker)
  220. break
  221. end
  222. end
  223. end
  224. # Returns the currently selected item as a String.
  225. def selection
  226. @matches[@selection]
  227. end
  228. def print_no_such_file_or_directory
  229. print_error 'NO SUCH FILE OR DIRECTORY'
  230. end
  231. private
  232. def set(setting, value)
  233. @settings ||= Settings.new
  234. @settings.set(setting, value)
  235. end
  236. def move_cursor_to_selected_line
  237. # on some non-GUI terminals, the cursor doesn't hide properly
  238. # so we move the cursor to prevent it from blinking away in the
  239. # upper-left corner in a distracting fashion
  240. @window.cursor = [@selection + 1, 0]
  241. end
  242. def print_error msg
  243. return unless VIM::Window.select(@window)
  244. unlock
  245. clear
  246. @window.height = @min_height > 0 ? @min_height : 1
  247. @@buffer[1] = "-- #{msg} --"
  248. lock
  249. end
  250. def restore_window_dimensions
  251. # sort from tallest to shortest, tie-breaking on window width
  252. @windows.sort! do |a, b|
  253. order = b.height <=> a.height
  254. if order.zero?
  255. b.width <=> a.width
  256. else
  257. order
  258. end
  259. end
  260. # starting with the tallest ensures that there are no constraints
  261. # preventing windows on the side of vertical splits from regaining
  262. # their original full size
  263. @windows.each do |w|
  264. # beware: window may be nil
  265. if window = ::VIM::Window[w.index]
  266. window.height = w.height
  267. window.width = w.width
  268. end
  269. end
  270. end
  271. def match_text_for_idx idx
  272. match = truncated_match @matches[idx].to_s
  273. if idx == @selection
  274. prefix = SELECTION_MARKER
  275. suffix = padding_for_selected_match match
  276. else
  277. if VIM::has_syntax? && VIM::has_conceal?
  278. match = match_with_syntax_highlight match
  279. end
  280. prefix = UNSELECTED_MARKER
  281. suffix = ''
  282. end
  283. prefix + match + suffix
  284. end
  285. # Highlight matching characters within the matched string.
  286. #
  287. # Note that this is only approximate; it will highlight the first matching
  288. # instances within the string, which may not actually be the instances that
  289. # were used by the matching/scoring algorithm to determine the best score
  290. # for the match.
  291. #
  292. def match_with_syntax_highlight match
  293. highlight_chars = @prompt.abbrev.downcase.chars.to_a
  294. match.chars.inject([]) do |output, char|
  295. if char.downcase == highlight_chars.first
  296. highlight_chars.shift
  297. output.concat [MH_START, char, MH_END]
  298. else
  299. output << char
  300. end
  301. end.join
  302. end
  303. # Print just the specified match.
  304. def print_match idx
  305. return unless VIM::Window.select(@window)
  306. unlock
  307. @@buffer[idx + 1] = match_text_for_idx idx
  308. lock
  309. end
  310. # Print all matches.
  311. def print_matches
  312. match_count = @matches.length
  313. if match_count == 0
  314. print_error 'NO MATCHES'
  315. else
  316. return unless VIM::Window.select(@window)
  317. unlock
  318. clear
  319. actual_lines = 1
  320. @window_width = @window.width # update cached value
  321. max_lines = VIM::Screen.lines - 5
  322. max_lines = 1 if max_lines < 0
  323. actual_lines = match_count < @min_height ? @min_height : match_count
  324. actual_lines = max_lines if actual_lines > max_lines
  325. @window.height = actual_lines
  326. (1..actual_lines).each do |line|
  327. idx = line - 1
  328. if @@buffer.count >= line
  329. @@buffer[line] = match_text_for_idx idx
  330. else
  331. @@buffer.append line - 1, match_text_for_idx(idx)
  332. end
  333. end
  334. lock
  335. end
  336. end
  337. # Prepare padding for match text (trailing spaces) so that selection
  338. # highlighting extends all the way to the right edge of the window.
  339. def padding_for_selected_match str
  340. len = str.length
  341. if len >= @window_width - MARKER_LENGTH
  342. ''
  343. else
  344. ' ' * (@window_width - MARKER_LENGTH - len)
  345. end
  346. end
  347. # Convert "really/long/path" into "really...path" based on available
  348. # window width.
  349. def truncated_match str
  350. len = str.length
  351. available_width = @window_width - MARKER_LENGTH
  352. return str if len <= available_width
  353. left = (available_width / 2) - 1
  354. right = (available_width / 2) - 2 + (available_width % 2)
  355. str[0, left] + '...' + str[-right, right]
  356. end
  357. def clear
  358. # range = % (whole buffer)
  359. # action = d (delete)
  360. # register = _ (black hole register, don't record deleted text)
  361. ::VIM::command 'silent %d _'
  362. end
  363. def get_cursor_highlight
  364. # there are 3 possible formats to check for, each needing to be
  365. # transformed in a certain way in order to reapply the highlight:
  366. # Cursor xxx guifg=bg guibg=fg -> :hi! Cursor guifg=bg guibg=fg
  367. # Cursor xxx links to SomethingElse -> :hi! link Cursor SomethingElse
  368. # Cursor xxx cleared -> :hi! clear Cursor
  369. highlight = VIM::capture 'silent! 0verbose highlight Cursor'
  370. if highlight =~ /^Cursor\s+xxx\s+links to (\w+)/
  371. "link Cursor #{$~[1]}"
  372. elsif highlight =~ /^Cursor\s+xxx\s+cleared/
  373. 'clear Cursor'
  374. elsif highlight =~ /Cursor\s+xxx\s+(.+)/
  375. "Cursor #{$~[1]}"
  376. else # likely cause E411 Cursor highlight group not found
  377. nil
  378. end
  379. end
  380. def hide_cursor
  381. if @cursor_highlight
  382. ::VIM::command 'highlight Cursor NONE'
  383. end
  384. end
  385. def show_cursor
  386. if @cursor_highlight
  387. ::VIM::command "highlight #{@cursor_highlight}"
  388. end
  389. end
  390. def lock
  391. set 'modifiable', false
  392. end
  393. def unlock
  394. set 'modifiable', true
  395. end
  396. end
  397. end