1 " Vim script.
  2 " Author: Peter Odding <peter@peterodding.com>
  3 " Last Change: August 22, 2010
  4 " URL: http://peterodding.com/code/vim/lua-inspect/
  5 " License: MIT
  6 
  7 let s:script = expand('<sfile>:p:~')
  8 
  9 function! luainspect#auto_enable() " {{{1
 10   if !&diff && !exists('b:luainspect_disabled')
 11     " Disable easytags.vim because it doesn't play nice with luainspect.vim!
 12     let b:easytags_nohl = 1
 13     " Define buffer local mappings for rename / goto definition features.
 14     inoremap <buffer> <silent> <F2> <C-o>:call luainspect#make_request('rename')<CR>
 15     nnoremap <buffer> <silent> <F2> :call luainspect#make_request('rename')<CR>
 16     nnoremap <buffer> <silent> gd :call luainspect#make_request('goto')<CR>
 17     " Enable balloon evaluation / dynamic tool tips.
 18     setlocal ballooneval balloonexpr=LuaInspectToolTip()
 19     " Install automatic commands to update the highlighting.
 20     for event in split(g:lua_inspect_events, ',')
 21       execute 'autocmd!' event '<buffer> LuaInspect'
 22     endfor
 23   endif
 24 endfunction
 25 
 26 function! luainspect#highlight_cmd(disable) " {{{1
 27   if a:disable
 28     call s:clear_previous_matches()
 29     unlet! b:luainspect_input
 30     unlet! b:luainspect_output
 31     unlet! b:luainspect_warnings
 32     let b:luainspect_disabled = 1
 33   else
 34     unlet! b:luainspect_disabled
 35     call luainspect#make_request('highlight')
 36   endif
 37 endfunction
 38 
 39 function! luainspect#make_request(action) " {{{1
 40   let starttime = xolox#timer#start()
 41   let bufnr = a:action != 'tooltip' ? bufnr('%') : v:beval_bufnr
 42   let bufname = bufname(bufnr)
 43   if bufname != ''
 44     let bufname = fnamemodify(bufname, ':p')
 45   endif
 46   if a:action == 'tooltip'
 47     let lines = getbufline(v:beval_bufnr, 1, "$")
 48     call insert(lines, v:beval_col)
 49     call insert(lines, v:beval_lnum)
 50   else
 51     let lines = getline(1, "$")
 52     call insert(lines, col('.'))
 53     call insert(lines, line('.'))
 54   endif
 55   call insert(lines, bufname)
 56   call insert(lines, a:action)
 57   call s:parse_text(join(lines, "\n"), s:prepare_search_path())
 58   if !empty(b:luainspect_output)
 59     let response = b:luainspect_output[0]
 60     if bufname == ''
 61       let friendlyname = 'buffer #' . bufnr
 62     else
 63       let friendlyname = fnamemodify(bufname, ':~')
 64     endif
 65     if response == 'syntax_error' && len(b:luainspect_output) >= 4
 66       " Never perform syntax error highlighting in non-Lua buffers!
 67       let linenum = b:luainspect_output[1] + 0
 68       let colnum = b:luainspect_output[2] + 0
 69       let linenum2 = b:luainspect_output[3] + 0
 70       let b:luainspect_syntax_error = b:luainspect_output[4]
 71       if a:action != 'tooltip' || v:beval_bufnr == bufnr('%')
 72         let error_cmd = 'syntax match luaInspectSyntaxError /\%%>%il\%%<%il.*/ containedin=ALLBUT,lua*Comment*'
 73         execute printf(error_cmd, linenum - 1, (linenum2 ? linenum2 : line('$')) + 1)
 74       endif
 75       call xolox#timer#stop("%s: Found a syntax error in %s in %s.", s:script, friendlyname, starttime)
 76       " But always let the user know that a syntax error exists.
 77       call xolox#warning("Syntax error around line %i in %s: %s", linenum, friendlyname, b:luainspect_syntax_error)
 78       return
 79     endif
 80     unlet! b:luainspect_syntax_error
 81     if response == 'highlight'
 82       call s:define_default_styles()
 83       call s:clear_previous_matches()
 84       call s:highlight_variables()
 85       call xolox#timer#stop("%s: Highlighted variables in %s in %s.", s:script, friendlyname, starttime)
 86     elseif response == 'goto'
 87       if len(b:luainspect_output) < 3
 88         call xolox#warning("No variable under cursor!")
 89       else
 90         let linenum = b:luainspect_output[1] + 0
 91         let colnum = b:luainspect_output[2] + 1
 92         call setpos('.', [0, linenum, colnum, 0])
 93         call xolox#timer#stop("%s: Jumped to definition in %s in %s.", s:script, friendlyname, starttime)
 94         if &verbose == 0
 95           " Clear previous "No variable under cursor!" message to avoid confusion.
 96           call xolox#message("")
 97         endif
 98       endif
 99     elseif response == 'tooltip'
100       if len(b:luainspect_output) > 1
101         call xolox#timer#stop("%s: Rendered tool tip for %s in %s.", s:script, friendlyname, starttime)
102         return join(b:luainspect_output[1:-1], "\n")
103       endif
104     elseif response == 'rename'
105       if len(b:luainspect_output) > 1
106         call xolox#timer#stop("%s: Prepared for rename in %s in %s.", s:script, friendlyname, starttime)
107         call s:rename_variable()
108       else
109         call xolox#warning("No variable under cursor!")
110       endif
111     endif
112   endif
113 endfunction
114 
115 function! s:prepare_search_path() " {{{1
116   let code = ''
117   if !(has('lua') && g:lua_inspect_internal && exists('s:changed_path'))
118     let template = 'package.path = ''%s/?.lua;'' .. package.path'
119     let code = printf(template, escape(expand(g:lua_inspect_path), '"\'''))
120     if has('lua') && g:lua_inspect_internal
121       execute 'lua' code
122       let s:changed_path = 1
123     endif
124   endif
125   return code
126 endfunction
127 
128 function! s:parse_text(input, search_path) " {{{1
129   if !(exists('b:luainspect_input')
130           \ && exists('b:luainspect_output')
131           \ && b:luainspect_input == a:input)
132     if !(has('lua') && g:lua_inspect_internal)
133       let template = 'lua -e "%s; require ''luainspect4vim'' (io.read ''*a'')"'
134       let command = printf(template, a:search_path)
135       try
136         let b:luainspect_output = xolox#shell#execute(command, 1, a:input)
137       catch /^Vim\%((\a\+)\)\=:E117/
138         " Ignore missing shell.vim plug-in.
139         let b:luainspect_output = split(system(command, a:input), "\n")
140         if v:shell_error
141           let msg = "Failed to execute LuaInspect as external process! %s"
142           throw printf(msg, strtrans(join(b:luainspect_output, "\n")))
143         endif
144       endtry
145     else
146       redir => output
147       silent lua require 'luainspect4vim' (vim.eval 'a:input')
148       redir END
149       let b:luainspect_output = split(output, "\n")
150     endif
151     " Remember the text that was just parsed.
152     let b:luainspect_input = a:input
153   endif
154 endfunction
155 
156 function! s:define_default_styles() " {{{1
157   " Always define the default highlighting styles
158   " (copied from /luainspect/scite.lua for consistency).
159   for [group, styles] in items(s:groups)
160     let group = 'luaInspect' . group
161     if type(styles) == type('')
162       let defgroup = styles
163     else
164       let defgroup = 'luaInspectDefault' . group
165       let style = &bg == 'light' ? styles[0] : styles[1]
166       execute 'highlight' defgroup style
167     endif
168     " Don't link the actual highlighting styles to the defaults if the user
169     " has already defined or linked the highlighting group. This enables color
170     " schemes and vimrc scripts to override the styles (see :help :hi-default).
171     execute 'highlight def link' group defgroup
172     unlet styles " to avoid E706.
173   endfor
174 endfunction
175 
176 function! s:clear_previous_matches() " {{{1
177   " Clear existing highlighting.
178   call clearmatches()
179   for group in keys(s:groups)
180     let group = 'luaInspect' . group
181     if hlexists(group)
182       execute 'syntax clear' group
183     endif
184   endfor
185 endfunction
186 
187 function! s:highlight_variables() " {{{1
188   call clearmatches()
189   let num_warnings = b:luainspect_output[1] + 0
190   call s:update_warnings(num_warnings > 0 ? b:luainspect_output[2 : num_warnings+1] : [])
191   let other_output = b:luainspect_output[num_warnings+2 : -1]
192   for line in other_output
193     if s:check_output(line, '^\w\+\(\s\+\d\+\)\{4}$')
194       let [group, l1, c1, l2, c2] = split(line)
195       " Convert strings to numbers.
196       let l1 += 0
197       let l2 += 0
198       " These adjustments were found by trial and error :-|
199       let c1 += 0
200       let c2 += 3
201       if group == 'luaInspectWrongArgCount'
202         call matchadd(group, s:highlight_position(l1, c1, l2, c2, 0))
203       elseif group == 'luaInspectSelectedVariable'
204         call matchadd(group, s:highlight_position(l1, c1, l2, c2, 1), 20)
205       else
206         let pattern = s:highlight_position(l1, c1, l2, c2, 1)
207         execute 'syntax match' group '/' . pattern . '/'
208       endif
209     endif
210   endfor
211 endfunction
212 
213 function! s:update_warnings(warnings) " {{{1
214   if !g:lua_inspect_warnings
215     return
216   endif
217   let list = []
218   for line in a:warnings
219     if s:check_output(line, '^line\s\+\d\+\s\+column\s\+\d\+\s\+-\s\+\S')
220       let fields = split(line)
221       let linenum = fields[1] + 0
222       let colnum = fields[3] + 0
223       let message = join(fields[5:-1])
224       call add(list, { 'bufnr': bufnr('%'), 'lnum': linenum, 'col': colnum, 'text': message })
225     endif
226   endfor
227   call setloclist(winnr(), list)
228   let b:luainspect_warnings = list
229   if !empty(list)
230     lopen
231     if winheight(winnr()) > 4
232       resize 4
233     endif
234     let warnings = len(list) > 1 ? 'warnings' : 'warning'
235     let w:quickfix_title = printf('%i %s reported by LuaInspect', len(list), warnings)
236     wincmd w
237   else
238     lclose
239   endif
240 endfunction
241 
242 function! s:rename_variable() " {{{1
243   " Highlight occurrences of variable before rename.
244   let highlights = []
245   for line in b:luainspect_output[1:-1]
246     if s:check_output(line, '^\d\+\(\s\+\d\+\)\{2}$')
247       let [l1, c1, c2] = split(line)
248       " Convert string to number.
249       let l1 += 0
250       " These adjustments were found by trial and error :-|
251       let c1 += 0
252       let c2 += 3
253       let pattern = s:highlight_position(l1, c1, l1, c2, 1)
254       call add(highlights, matchadd('IncSearch', pattern))
255     endif
256   endfor
257   redraw
258   " Prompt for new name.
259   let oldname = expand('<cword>')
260   let prompt = "Please enter the new name for %s: "
261   let newname = input(printf(prompt, oldname), oldname)
262   " Clear highlighting of occurrences.
263   call map(highlights, 'matchdelete(v:val)')
264   " Perform rename?
265   if newname != '' && newname !=# oldname
266     let num_renamed = 0
267     for fields in reverse(b:luainspect_output[1:-1])
268       let [linenum, firstcol, lastcol] = split(fields)
269       " Convert string to number.
270       let linenum += 0
271       " These adjustments were found by trial and error :-|
272       let firstcol -= 1
273       let lastcol += 1
274       let line = getline(linenum)
275       let prefix = firstcol > 0 ? line[0 : firstcol] : ''
276       let suffix = lastcol < len(line) ? line[lastcol : -1] : ''
277       call setline(linenum, prefix . newname . suffix)
278       let num_renamed += 1
279     endfor
280     let msg = "Renamed %i occurrences of %s to %s"
281     call xolox#message(msg, num_renamed, oldname, newname)
282   endif
283 endfunction
284 
285 function! s:check_output(line, pattern) " {{{1
286   if match(a:line, a:pattern) >= 0
287     return 1
288   else
289     call xolox#warning("Invalid output from luainspect4vim.lua: '%s'", strtrans(a:line))
290     return 0
291   endif
292 endfunction
293 
294 function! s:highlight_position(l1, c1, l2, c2, ident_only) " {{{1
295   let l1 = a:l1 >= 1 ? (a:l1 - 1) : a:l1
296   let p = '\%>' . l1 . 'l\%>' . a:c1 . 'c'
297   let p .= a:ident_only ? '\<\w\+\>' : '\_.\+'
298   return p . '\%<' . (a:l2 + 1) . 'l\%<' . a:c2 . 'c'
299 endfunction
300 
301 " Highlighting groups and their default light/dark styles. {{{1
302 
303 let s:groups = {}
304 let s:groups['GlobalDefined'] = ['guifg=#600000', 'guifg=#ffc080']
305 let s:groups['GlobalUndefined'] = 'ErrorMsg'
306 let s:groups['LocalUnused'] = ['guifg=#ffffff guibg=#000080', 'guifg=#ffffff guibg=#000080']
307 let s:groups['LocalMutated'] = ['gui=italic guifg=#000080', 'gui=italic guifg=#c0c0ff']
308 let s:groups['UpValue'] = ['guifg=#0000ff', 'guifg=#e8e8ff']
309 let s:groups['Param'] = ['guifg=#000040', 'guifg=#8080ff']
310 let s:groups['Local'] = ['guifg=#000040', 'guifg=#c0c0ff']
311 let s:groups['FieldDefined'] = ['guifg=#600000', 'guifg=#ffc080']
312 let s:groups['FieldUndefined'] = ['guifg=#c00000', 'guifg=#ff0000']
313 let s:groups['SelectedVariable'] = 'CursorLine'
314 let s:groups['SyntaxError'] = 'SpellBad'
315 let s:groups['WrongArgCount'] = 'SpellLocal'