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