Skip to content

Commit d1cb103

Browse files
committed
Add HdevtoolsInfo command
1 parent 30d1983 commit d1cb103

File tree

2 files changed

+384
-1
lines changed

2 files changed

+384
-1
lines changed

autoload/hdevtools.vim

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,383 @@
1+
let s:hdevtools_info_buffer = -1
2+
3+
function! hdevtools#info(identifier)
4+
let l:identifier = a:identifier
5+
6+
if l:identifier ==# ''
7+
" No identifier argument given, probably called from a keyboard shortcut
8+
9+
if bufnr('%') == s:hdevtools_info_buffer
10+
" The Info Window is already open and active, so simply close it and
11+
" finish
12+
call hdevtools#infowin_leave()
13+
return
14+
endif
15+
16+
" Get the identifier under the cursor
17+
let l:identifier = s:extract_identifier(getline("."), col("."))
18+
endif
19+
20+
if l:identifier ==# ''
21+
echo '-- No Identifier Under Cursor'
22+
return
23+
endif
24+
25+
let l:file = expand('%')
26+
if l:file ==# ''
27+
call hdevtools#print_warning("current version of hdevtools.vim doesn't support running on an unnamed buffer.")
28+
return
29+
endif
30+
let l:cmd = hdevtools#build_command('info', shellescape(l:file) . ' -- ' . shellescape(l:identifier))
31+
let l:output = system(l:cmd)
32+
33+
let l:lines = split(l:output, '\n')
34+
35+
" Check if the call to hdevtools info succeeded
36+
if v:shell_error != 0
37+
for l:line in l:lines
38+
call hdevtools#print_error(l:line)
39+
endfor
40+
return
41+
endif
42+
43+
" Create a new window
44+
call s:infowin_create("(" . l:identifier . ")")
45+
46+
" Adjust the height of the Info Window so that all lines will fit
47+
exe 'resize ' (len(l:lines) + 1)
48+
49+
" The result returned from the 'info' command is very similar to regular
50+
" haskell code, so Haskell syntax highlighting looks good on it
51+
setlocal filetype=haskell
52+
53+
" Fill the contents of the Info Window with the result
54+
setlocal modifiable
55+
call append(0, l:lines)
56+
setlocal nomodifiable
57+
58+
" Jump the cursor to the beginning of the buffer
59+
normal gg
60+
61+
" Look for the first line containing a reference to a file and jump the
62+
" cursor to it if found
63+
for l:i in range(0, len(l:lines)-1)
64+
if match(l:lines[l:i], '-- Defined at \S\+:\d\+:\d\+') >= 0
65+
call setpos(".", [0, l:i + 1, 1, 0])
66+
break
67+
endif
68+
endfor
69+
70+
" Apply syntax highlighting for these comments: -- Defined at Hello.hs:12:5
71+
" These are turned into links that can be jumped to
72+
syntax match HdevtoolsInfoLink '-- Defined at \zs\S\+:\d\+:\d\+' containedin=ALL contained
73+
highlight link HdevtoolsInfoLink Underlined
74+
endfunction
75+
76+
function! s:extract_identifier(line_text, col)
77+
if a:col > len(a:line_text)
78+
return ''
79+
endif
80+
81+
let l:index = a:col - 1
82+
let l:delimiter = '\s\|[(),;`{}"[\]]'
83+
84+
" Move the index forward till the cursor is not on a delimiter
85+
while match(a:line_text[l:index], l:delimiter) == 0
86+
let l:index = l:index + 1
87+
if l:index == len(a:line_text)
88+
return ''
89+
endif
90+
endwhile
91+
92+
let l:start_index = l:index
93+
" Move start_index backwards until it hits a delimiter or beginning of line
94+
while l:start_index > 0 && match(a:line_text[l:start_index-1], l:delimiter) < 0
95+
let l:start_index = l:start_index - 1
96+
endwhile
97+
98+
let l:end_index = l:index
99+
" Move end_index forwards until it hits a delimiter or end of line
100+
while l:end_index < len(a:line_text) - 1 && match(a:line_text[l:end_index+1], l:delimiter) < 0
101+
let l:end_index = l:end_index + 1
102+
endwhile
103+
104+
let l:fragment = a:line_text[l:start_index : l:end_index]
105+
let l:index = l:index - l:start_index
106+
107+
let l:results = []
108+
109+
let l:name_regex = '\(\u\(\w\|''\)*\.\)*\(\a\|_\)\(\w\|''\)*'
110+
let l:operator_regex = '\(\u\(\w\|''\)*\.\)*\(\\\|[-!#$%&*+./<=>?@^|~:]\)\+'
111+
112+
" Perform two passes over the fragment(one for finding a name, and the other
113+
" for finding an operator). Each pass tries to find a match that has the
114+
" cursor contained within it.
115+
for l:regex in [l:name_regex, l:operator_regex]
116+
let l:remainder = l:fragment
117+
let l:rindex = l:index
118+
while 1
119+
let l:i = match(l:remainder, l:regex)
120+
if l:i < 0
121+
break
122+
endif
123+
let l:result = matchstr(l:remainder, l:regex)
124+
let l:end = l:i + len(l:result)
125+
if l:i <= l:rindex && l:end > l:rindex
126+
call add(l:results, l:result)
127+
break
128+
endif
129+
let l:remainder = l:remainder[l:end :]
130+
let l:rindex = l:rindex - l:end
131+
endwhile
132+
endfor
133+
134+
" There can be at most 2 matches(one from each pass). The longest one is the
135+
" correct one.
136+
if len(l:results) == 0
137+
return ''
138+
elseif len(l:results) == 1
139+
return l:results[0]
140+
else
141+
if len(l:results[0]) > len(l:results[1])
142+
return l:results[0]
143+
else
144+
return l:results[1]
145+
endif
146+
endif
147+
endfunction
148+
149+
" Unit Test
150+
function! hdevtools#test_extract_identifier()
151+
let l:tests = [
152+
\ 'let #foo# = 5',
153+
\ '#main#',
154+
\ '1 #+# 1',
155+
\ '1#+#1',
156+
\ 'blah #Foo.Bar# blah',
157+
\ 'blah #Foo.bar# blah',
158+
\ 'blah #foo#.Bar blah',
159+
\ 'blah #foo#.bar blah',
160+
\ 'blah foo#.#Bar blah',
161+
\ 'blah foo#.#bar blah',
162+
\ 'blah foo.#bar# blah',
163+
\ 'blah foo.#Bar# blah',
164+
\ 'blah #A.B.C.d# blah',
165+
\ '#foo#+bar',
166+
\ 'foo+#bar#',
167+
\ '#Foo#+bar',
168+
\ 'foo+#Bar#',
169+
\ '#Prelude..#',
170+
\ '[#foo#..bar]',
171+
\ '[foo..#bar#]',
172+
\ '#Foo.bar#',
173+
\ '#Foo#*bar',
174+
\ 'Foo#*#bar',
175+
\ 'Foo*#bar#',
176+
\ '#Foo.foo#.bar',
177+
\ 'Foo.foo#.#bar',
178+
\ 'Foo.foo.#bar#',
179+
\ '"a"#++#"b"',
180+
\ '''a''#<#''b''',
181+
\ '#Foo.$#',
182+
\ 'foo.#Foo.$#',
183+
\ '#-#',
184+
\ '#/#',
185+
\ '#\#',
186+
\ '#@#'
187+
\ ]
188+
for l:test in l:tests
189+
let l:expected = matchstr(l:test, '#\zs.*\ze#')
190+
let l:input = substitute(l:test, '#', '', 'g')
191+
let l:start_index = match(l:test, '#') + 1
192+
let l:end_index = match(l:test, '\%>' . l:start_index . 'c#') - 1
193+
for l:i in range(l:start_index, l:end_index)
194+
let l:result = s:extract_identifier(l:input, l:i)
195+
if l:expected !=# l:result
196+
call hdevtools#print_error("TEST FAILED expected: (" . l:expected . ") got: (" . l:result . ") for column " . l:i . " of: " . l:input)
197+
endif
198+
endfor
199+
endfor
200+
endfunction
201+
202+
" ----------------------------------------------------------------------------
203+
" The window code below was adapted from the 'Command-T' plugin, with major
204+
" changes (and translated from the original Ruby)
205+
"
206+
" Command-T:
207+
" https://wincent.com/products/command-t/
208+
209+
function! s:infowin_create(window_title)
210+
let s:initial_window = winnr()
211+
call s:window_dimensions_save()
212+
213+
" The following settings are global, so they must be saved before being
214+
" changed so that they can be later restored.
215+
" If you add to the code below changes to additional global settings, then
216+
" you must also appropriately modify s:settings_save and s:settings_restore
217+
call s:settings_save()
218+
set noinsertmode " don't make Insert mode the default
219+
set report=9999 " don't show 'X lines changed' reports
220+
set sidescroll=0 " don't sidescroll in jumps
221+
set sidescrolloff=0 " don't sidescroll automatically
222+
set noequalalways " don't auto-balance window sizes
223+
224+
" The following settings are local so they don't have to be saved
225+
exe 'silent! botright 1split' fnameescape(a:window_title)
226+
setlocal bufhidden=unload " unload buf when no longer displayed
227+
setlocal buftype=nofile " buffer is not related to any file
228+
setlocal nomodifiable " prevent manual edits
229+
setlocal noswapfile " don't create a swapfile
230+
setlocal nowrap " don't soft-wrap
231+
setlocal nonumber " don't show line numbers
232+
setlocal nolist " don't use List mode (visible tabs etc)
233+
setlocal foldcolumn=0 " don't show a fold column at side
234+
setlocal foldlevel=99 " don't fold anything
235+
setlocal nocursorline " don't highlight line cursor is on
236+
setlocal nospell " spell-checking off
237+
setlocal nobuflisted " don't show up in the buffer list
238+
setlocal textwidth=0 " don't hard-wrap (break long lines)
239+
240+
" Save the buffer number of the Info Window for later
241+
let s:hdevtools_info_buffer = bufnr("%")
242+
243+
" Key bindings for the Info Window
244+
nnoremap <silent> <buffer> <CR> :call hdevtools#infowin_jump()<CR>
245+
nnoremap <silent> <buffer> <C-CR> :call hdevtools#infowin_jump('sp')<CR>
246+
nnoremap <silent> <buffer> <ESC> :call hdevtools#infowin_leave()<CR>
247+
248+
" perform cleanup using an autocmd to ensure we don't get caught out by some
249+
" unexpected means of dismissing or leaving the Info Window (eg. <C-W q>,
250+
" <C-W k> etc)
251+
autocmd! * <buffer>
252+
autocmd BufLeave <buffer> silent! call hdevtools#infowin_leave()
253+
autocmd BufUnload <buffer> silent! call s:infowin_unload()
254+
endfunction
255+
256+
function! s:settings_save()
257+
" The following must be in sync with settings_restore
258+
let s:original_settings = [
259+
\ &report,
260+
\ &sidescroll,
261+
\ &sidescrolloff,
262+
\ &equalalways,
263+
\ &insertmode
264+
\ ]
265+
endfunction
266+
267+
function! s:settings_restore()
268+
" The following must be in sync with settings_save
269+
let &report = s:original_settings[0]
270+
let &sidescroll = s:original_settings[1]
271+
let &sidescrolloff = s:original_settings[2]
272+
let &equalalways = s:original_settings[3]
273+
let &insertmode = s:original_settings[4]
274+
endfunction
275+
276+
function! s:window_dimensions_save()
277+
" Each element of the list s:window_dimensions is a list of 3 integers of
278+
" the form: [id, width, height]
279+
let s:window_dimensions = []
280+
for l:i in range(1, winnr("$"))
281+
call add(s:window_dimensions, [l:i, winwidth(i), winheight(i)])
282+
endfor
283+
endfunction
284+
285+
" Used in s:window_dimensions_restore for sorting the windows
286+
function! hdevtools#compare_window(i1, i2)
287+
" Compare the window heights:
288+
if a:i1[2] < a:i2[2]
289+
return 1
290+
elseif a:i1[2] > a:i2[2]
291+
return -1
292+
endif
293+
" The heights were equal, so compare the widths:
294+
if a:i1[1] < a:i2[1]
295+
return 1
296+
elseif a:i1[1] > a:i2[1]
297+
return -1
298+
endif
299+
" The widths were also equal:
300+
return 0
301+
endfunction
302+
303+
function! s:window_dimensions_restore()
304+
" sort from tallest to shortest, tie-breaking on window width
305+
call sort(s:window_dimensions, "hdevtools#compare_window")
306+
307+
" starting with the tallest ensures that there are no constraints preventing
308+
" windows on the side of vertical splits from regaining their original full
309+
" size
310+
for l:i in s:window_dimensions
311+
let l:id = l:i[0]
312+
let l:width = l:i[1]
313+
let l:height = l:i[2]
314+
exe l:id . "wincmd w"
315+
exe "resize" l:height
316+
exe "vertical resize" l:width
317+
endfor
318+
endfunction
319+
320+
function! hdevtools#infowin_leave()
321+
call s:infowin_close()
322+
call s:infowin_unload()
323+
let s:hdevtools_info_buffer = -1
324+
endfunction
325+
326+
function! s:infowin_unload()
327+
call s:window_dimensions_restore()
328+
call s:settings_restore()
329+
exe s:initial_window . "wincmd w"
330+
endfunction
331+
332+
function! s:infowin_close()
333+
exe "silent! bunload!" s:hdevtools_info_buffer
334+
endfunction
335+
336+
" Jumps to the location under the cursor.
337+
"
338+
" An single optional argument is allowed, which is a string command for
339+
" opening a window, for example 'split' or 'vsplit'.
340+
"
341+
" If no argument is supplied then the default is to try to reuse the existing
342+
" window (using 'edit') unless it is unsaved and cannot be changed, in which
343+
" case 'split' is used
344+
function! hdevtools#infowin_jump(...)
345+
" Search for the filepath, line and column in the current line that matches
346+
" the format: -- Defined at Hello.hs:12:5
347+
let l:line = getline(".")
348+
let l:m = matchlist(line, '-- Defined at \(\S\+\):\(\d\+\):\(\d\+\)')
349+
350+
if len(l:m) == 0
351+
" No match found on the current line
352+
return
353+
endif
354+
355+
" Extract the values from the result of the previous regex
356+
let l:filepath = l:m[1]
357+
let l:row = l:m[2]
358+
let l:col = l:m[3]
359+
360+
" Get rid of the Info Window; the user doesn't need it anymore
361+
call hdevtools#infowin_leave()
362+
363+
" Open the file in a window as appropriate
364+
if a:0 > 0 && a:1 !=# ''
365+
exe "silent" a:1 fnameescape(l:filepath)
366+
else
367+
if l:filepath !=# bufname("%")
368+
if !&hidden && &modified
369+
let l:opencmd = "sp"
370+
else
371+
let l:opencmd = "e"
372+
endif
373+
exe "silent" l:opencmd fnameescape(l:filepath)
374+
endif
375+
endif
376+
377+
" Jump the cursor to the position from the 'Defined at'
378+
call setpos(".", [0, l:row, l:col, 0])
379+
endfunction
380+
1381
" ----------------------------------------------------------------------------
2382
" Most of the code below has been taken from ghcmod-vim, with a few
3383
" adjustments and tweaks.

ftplugin/haskell/hdevtools.vim

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ endif
2626

2727
command! -buffer -nargs=0 HdevtoolsType echo hdevtools#type()[1]
2828
command! -buffer -nargs=0 HdevtoolsClear call hdevtools#type_clear()
29+
command! -buffer -nargs=? HdevtoolsInfo call hdevtools#info(<q-args>)
30+
2931
let b:undo_ftplugin .= join(map([
3032
\ 'HdevtoolsType',
31-
\ 'HdevtoolsClear'
33+
\ 'HdevtoolsClear',
34+
\ 'HdevtoolsInfo'
3235
\ ], '"delcommand " . v:val'), ' | ')
3336
let b:undo_ftplugin .= ' | unlet b:did_ftplugin_hdevtools'

0 commit comments

Comments
 (0)