]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/util.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / util.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Contains miscellaneous functions
3
4 " A wrapper function for mode() so we can test calls for it.
5 function! ale#util#Mode(...) abort
6     return call('mode', a:000)
7 endfunction
8
9 " A wrapper function for feedkeys so we can test calls for it.
10 function! ale#util#FeedKeys(...) abort
11     return call('feedkeys', a:000)
12 endfunction
13
14 " Show a message in as small a window as possible.
15 "
16 " Vim 8 does not support echoing long messages from asynchronous callbacks,
17 " but NeoVim does. Small messages can be echoed in Vim 8, and larger messages
18 " have to be shown in preview windows.
19 function! ale#util#ShowMessage(string, ...) abort
20     let l:options = get(a:000, 0, {})
21
22     if !has('nvim')
23         call ale#preview#CloseIfTypeMatches('ale-preview.message')
24     endif
25
26     " We have to assume the user is using a monospace font.
27     if has('nvim') || (a:string !~? "\n" && len(a:string) < &columns)
28         " no-custom-checks
29         echo a:string
30     else
31         call ale#preview#Show(split(a:string, "\n"), extend(
32         \   {
33         \       'filetype': 'ale-preview.message',
34         \       'stay_here': 1,
35         \   },
36         \   l:options,
37         \))
38     endif
39 endfunction
40
41 " A wrapper function for execute, so we can test executing some commands.
42 function! ale#util#Execute(expr) abort
43     execute a:expr
44 endfunction
45
46 if !exists('g:ale#util#nul_file')
47     " A null file for sending output to nothing.
48     let g:ale#util#nul_file = '/dev/null'
49
50     if has('win32')
51         let g:ale#util#nul_file = 'nul'
52     endif
53 endif
54
55 " Given a job, a buffered line of data, a list of parts of lines, a mode data
56 " is being read in, and a callback, join the lines of output for a NeoVim job
57 " or socket together, and call the callback with the joined output.
58 "
59 " Note that jobs and IDs are the same thing on NeoVim.
60 function! ale#util#JoinNeovimOutput(job, last_line, data, mode, callback) abort
61     if a:mode is# 'raw'
62         call a:callback(a:job, join(a:data, "\n"))
63
64         return ''
65     endif
66
67     let l:lines = a:data[:-2]
68
69     if len(a:data) > 1
70         let l:lines[0] = a:last_line . l:lines[0]
71         let l:new_last_line = a:data[-1]
72     else
73         let l:new_last_line = a:last_line . get(a:data, 0, '')
74     endif
75
76     for l:line in l:lines
77         call a:callback(a:job, l:line)
78     endfor
79
80     return l:new_last_line
81 endfunction
82
83 " Return the number of lines for a given buffer.
84 function! ale#util#GetLineCount(buffer) abort
85     return len(getbufline(a:buffer, 1, '$'))
86 endfunction
87
88 function! ale#util#GetFunction(string_or_ref) abort
89     if type(a:string_or_ref) is v:t_string
90         return function(a:string_or_ref)
91     endif
92
93     return a:string_or_ref
94 endfunction
95
96 " Open the file (at the given line).
97 " options['open_in'] can be:
98 "   current-buffer (default)
99 "   tab
100 "   split
101 "   vsplit
102 function! ale#util#Open(filename, line, column, options) abort
103     let l:open_in = get(a:options, 'open_in', 'current-buffer')
104     let l:args_to_open = '+' . a:line . ' ' . fnameescape(a:filename)
105
106     if l:open_in is# 'tab'
107         call ale#util#Execute('tabedit ' . l:args_to_open)
108     elseif l:open_in is# 'split'
109         call ale#util#Execute('split ' . l:args_to_open)
110     elseif l:open_in is# 'vsplit'
111         call ale#util#Execute('vsplit ' . l:args_to_open)
112     elseif bufnr(a:filename) isnot bufnr('')
113         " Open another file only if we need to.
114         call ale#util#Execute('edit ' . l:args_to_open)
115     else
116         normal! m`
117     endif
118
119     call cursor(a:line, a:column)
120     normal! zz
121 endfunction
122
123 let g:ale#util#error_priority = 5
124 let g:ale#util#warning_priority = 4
125 let g:ale#util#info_priority = 3
126 let g:ale#util#style_error_priority = 2
127 let g:ale#util#style_warning_priority = 1
128
129 function! ale#util#GetItemPriority(item) abort
130     if a:item.type is# 'I'
131         return g:ale#util#info_priority
132     endif
133
134     if a:item.type is# 'W'
135         if get(a:item, 'sub_type', '') is# 'style'
136             return g:ale#util#style_warning_priority
137         endif
138
139         return g:ale#util#warning_priority
140     endif
141
142     if get(a:item, 'sub_type', '') is# 'style'
143         return g:ale#util#style_error_priority
144     endif
145
146     return g:ale#util#error_priority
147 endfunction
148
149 " Compare two loclist items for ALE, sorted by their buffers, filenames, and
150 " line numbers and column numbers.
151 function! ale#util#LocItemCompare(left, right) abort
152     if a:left.bufnr < a:right.bufnr
153         return -1
154     endif
155
156     if a:left.bufnr > a:right.bufnr
157         return 1
158     endif
159
160     if a:left.bufnr == -1
161         if a:left.filename < a:right.filename
162             return -1
163         endif
164
165         if a:left.filename > a:right.filename
166             return 1
167         endif
168     endif
169
170     if a:left.lnum < a:right.lnum
171         return -1
172     endif
173
174     if a:left.lnum > a:right.lnum
175         return 1
176     endif
177
178     if a:left.col < a:right.col
179         return -1
180     endif
181
182     if a:left.col > a:right.col
183         return 1
184     endif
185
186     " When either of the items lacks a problem type, then the two items should
187     " be considered equal. This is important for loclist jumping.
188     if !has_key(a:left, 'type') || !has_key(a:right, 'type')
189         return 0
190     endif
191
192     let l:left_priority = ale#util#GetItemPriority(a:left)
193     let l:right_priority = ale#util#GetItemPriority(a:right)
194
195     if l:left_priority < l:right_priority
196         return -1
197     endif
198
199     if l:left_priority > l:right_priority
200         return 1
201     endif
202
203     return 0
204 endfunction
205
206 " Compare two loclist items, including the text for the items.
207 "
208 " This function can be used for de-duplicating lists.
209 function! ale#util#LocItemCompareWithText(left, right) abort
210     let l:cmp_value = ale#util#LocItemCompare(a:left, a:right)
211
212     if l:cmp_value
213         return l:cmp_value
214     endif
215
216     if a:left.text < a:right.text
217         return -1
218     endif
219
220     if a:left.text > a:right.text
221         return 1
222     endif
223
224     return 0
225 endfunction
226
227 " This function will perform a binary search and a small sequential search
228 " on the list to find the last problem in the buffer and line which is
229 " on or before the column. The index of the problem will be returned.
230 "
231 " -1 will be returned if nothing can be found.
232 function! ale#util#BinarySearch(loclist, buffer, line, column) abort
233     let l:min = 0
234     let l:max = len(a:loclist) - 1
235
236     while 1
237         if l:max < l:min
238             return -1
239         endif
240
241         let l:mid = (l:min + l:max) / 2
242         let l:item = a:loclist[l:mid]
243
244         " Binary search for equal buffers, equal lines, then near columns.
245         if l:item.bufnr < a:buffer
246             let l:min = l:mid + 1
247         elseif l:item.bufnr > a:buffer
248             let l:max = l:mid - 1
249         elseif l:item.lnum < a:line
250             let l:min = l:mid + 1
251         elseif l:item.lnum > a:line
252             let l:max = l:mid - 1
253         else
254             " This part is a small sequential search.
255             let l:index = l:mid
256
257             " Search backwards to find the first problem on the line.
258             while l:index > 0
259             \&& a:loclist[l:index - 1].bufnr == a:buffer
260             \&& a:loclist[l:index - 1].lnum == a:line
261                 let l:index -= 1
262             endwhile
263
264             " Find the last problem on or before this column.
265             while l:index < l:max
266             \&& a:loclist[l:index + 1].bufnr == a:buffer
267             \&& a:loclist[l:index + 1].lnum == a:line
268             \&& a:loclist[l:index + 1].col <= a:column
269                 let l:index += 1
270             endwhile
271
272             " Scan forwards to find the last item on the column for the item
273             " we found, which will have the most serious problem.
274             let l:item_column = a:loclist[l:index].col
275
276             while l:index < l:max
277             \&& a:loclist[l:index + 1].bufnr == a:buffer
278             \&& a:loclist[l:index + 1].lnum == a:line
279             \&& a:loclist[l:index + 1].col == l:item_column
280                 let l:index += 1
281             endwhile
282
283             return l:index
284         endif
285     endwhile
286 endfunction
287
288 " A function for testing if a function is running inside a sandbox.
289 " See :help sandbox
290 function! ale#util#InSandbox() abort
291     try
292         let &l:equalprg=&l:equalprg
293     catch /E48/
294         " E48 is the sandbox error.
295         return 1
296     endtry
297
298     return 0
299 endfunction
300
301 function! ale#util#Tempname() abort
302     let l:clear_tempdir = 0
303
304     if exists('$TMPDIR') && empty($TMPDIR)
305         let l:clear_tempdir = 1
306         let $TMPDIR = '/tmp'
307     endif
308
309     try
310         let l:name = tempname() " no-custom-checks
311     finally
312         if l:clear_tempdir
313             let $TMPDIR = ''
314         endif
315     endtry
316
317     return l:name
318 endfunction
319
320 " Given a single line, or a List of lines, and a single pattern, or a List
321 " of patterns, return all of the matches for the lines(s) from the given
322 " patterns, using matchlist().
323 "
324 " Only the first pattern which matches a line will be returned.
325 function! ale#util#GetMatches(lines, patterns) abort
326     let l:matches = []
327     let l:lines = type(a:lines) is v:t_list ? a:lines : [a:lines]
328     let l:patterns = type(a:patterns) is v:t_list ? a:patterns : [a:patterns]
329
330     for l:line in l:lines
331         for l:pattern in l:patterns
332             let l:match = matchlist(l:line, l:pattern)
333
334             if !empty(l:match)
335                 call add(l:matches, l:match)
336                 break
337             endif
338         endfor
339     endfor
340
341     return l:matches
342 endfunction
343
344 " Given a single line, or a List of lines, and a single pattern, or a List of
345 " patterns, and a callback function for mapping the items matches, return the
346 " result of mapping all of the matches for the lines from the given patterns,
347 " using matchlist()
348 "
349 " Only the first pattern which matches a line will be returned.
350 function! ale#util#MapMatches(lines, patterns, Callback) abort
351     return map(ale#util#GetMatches(a:lines, a:patterns), 'a:Callback(v:val)')
352 endfunction
353
354 function! s:LoadArgCount(function) abort
355     try
356         let l:output = execute('function a:function')
357     catch /E123/
358         return 0
359     endtry
360
361     let l:match = matchstr(split(l:output, "\n")[0], '\v\([^)]+\)')[1:-2]
362     let l:arg_list = filter(split(l:match, ', '), 'v:val isnot# ''...''')
363
364     return len(l:arg_list)
365 endfunction
366
367 " Given the name of a function, a Funcref, or a lambda, return the number
368 " of named arguments for a function.
369 function! ale#util#FunctionArgCount(function) abort
370     let l:Function = ale#util#GetFunction(a:function)
371     let l:count = s:LoadArgCount(l:Function)
372
373     " If we failed to get the count, forcibly load the autoload file, if the
374     " function is an autoload function. autoload functions aren't normally
375     " defined until they are called.
376     if l:count == 0
377         let l:function_name = matchlist(string(l:Function), 'function([''"]\(.\+\)[''"])')[1]
378
379         if l:function_name =~# '#'
380             execute 'runtime autoload/' . join(split(l:function_name, '#')[:-2], '/') . '.vim'
381             let l:count = s:LoadArgCount(l:Function)
382         endif
383     endif
384
385     return l:count
386 endfunction
387
388 " Escape a string so the characters in it will be safe for use inside of PCRE
389 " or RE2 regular expressions without characters having special meanings.
390 function! ale#util#EscapePCRE(unsafe_string) abort
391     return substitute(a:unsafe_string, '\([\-\[\]{}()*+?.^$|]\)', '\\\1', 'g')
392 endfunction
393
394 " Escape a string so that it can be used as a literal string inside an evaled
395 " vim command.
396 function! ale#util#EscapeVim(unsafe_string) abort
397     return "'" . substitute(a:unsafe_string, "'", "''", 'g') . "'"
398 endfunction
399
400
401 " Given a String or a List of String values, try and decode the string(s)
402 " as a JSON value which can be decoded with json_decode. If the JSON string
403 " is invalid, the default argument value will be returned instead.
404 "
405 " This function is useful in code where the data can't be trusted to be valid
406 " JSON, and where throwing exceptions is mostly just irritating.
407 function! ale#util#FuzzyJSONDecode(data, default) abort
408     if empty(a:data)
409         return a:default
410     endif
411
412     let l:str = type(a:data) is v:t_string ? a:data : join(a:data, '')
413
414     try
415         let l:result = json_decode(l:str)
416
417         " Vim 8 only uses the value v:none for decoding blank strings.
418         if !has('nvim') && l:result is v:none
419             return a:default
420         endif
421
422         return l:result
423     catch /E474\|E491/
424         return a:default
425     endtry
426 endfunction
427
428 " Write a file, including carriage return characters for DOS files.
429 "
430 " The buffer number is required for determining the fileformat setting for
431 " the buffer.
432 function! ale#util#Writefile(buffer, lines, filename) abort
433     let l:corrected_lines = getbufvar(a:buffer, '&fileformat') is# 'dos'
434     \   ? map(copy(a:lines), 'substitute(v:val, ''\r*$'', ''\r'', '''')')
435     \   : a:lines
436
437     " Set binary flag if buffer doesn't have eol and nofixeol to avoid appending newline
438     let l:flags = !getbufvar(a:buffer, '&eol') && exists('+fixeol') && !&fixeol ? 'bS' : 'S'
439
440     call writefile(l:corrected_lines, a:filename, l:flags) " no-custom-checks
441 endfunction
442
443 if !exists('s:patial_timers')
444     let s:partial_timers = {}
445 endif
446
447 function! s:ApplyPartialTimer(timer_id) abort
448     if has_key(s:partial_timers, a:timer_id)
449         let [l:Callback, l:args] = remove(s:partial_timers, a:timer_id)
450         call call(l:Callback, [a:timer_id] + l:args)
451     endif
452 endfunction
453
454 " Given a delay, a callback, a List of arguments, start a timer with
455 " timer_start() and call the callback provided with [timer_id] + args.
456 "
457 " The timer must not be stopped with timer_stop().
458 " Use ale#util#StopPartialTimer() instead, which can stop any timer, and will
459 " clear any arguments saved for executing callbacks later.
460 function! ale#util#StartPartialTimer(delay, callback, args) abort
461     let l:timer_id = timer_start(a:delay, function('s:ApplyPartialTimer'))
462     let s:partial_timers[l:timer_id] = [a:callback, a:args]
463
464     return l:timer_id
465 endfunction
466
467 function! ale#util#StopPartialTimer(timer_id) abort
468     call timer_stop(a:timer_id)
469
470     if has_key(s:partial_timers, a:timer_id)
471         call remove(s:partial_timers, a:timer_id)
472     endif
473 endfunction
474
475 " Given a possibly multi-byte string and a 1-based character position on a
476 " line, return the 1-based byte position on that line.
477 function! ale#util#Col(str, chr) abort
478     if a:chr < 2
479         return a:chr
480     endif
481
482     return strlen(join(split(a:str, '\zs')[0:a:chr - 2], '')) + 1
483 endfunction
484
485 function! ale#util#FindItemAtCursor(buffer) abort
486     let l:info = get(g:ale_buffer_info, a:buffer, {})
487     let l:loclist = get(l:info, 'loclist', [])
488     let l:pos = getpos('.')
489     let l:index = ale#util#BinarySearch(l:loclist, a:buffer, l:pos[1], l:pos[2])
490     let l:loc = l:index >= 0 ? l:loclist[l:index] : {}
491
492     return [l:info, l:loc]
493 endfunction
494
495 function! ale#util#Input(message, value, ...) abort
496     if a:0 > 0
497         return input(a:message, a:value, a:1)
498     else
499         return input(a:message, a:value)
500     endif
501 endfunction
502
503 function! ale#util#HasBuflineApi() abort
504     return exists('*deletebufline') && exists('*setbufline')
505 endfunction
506
507 " Sets buffer contents to lines
508 function! ale#util#SetBufferContents(buffer, lines) abort
509     let l:has_bufline_api = ale#util#HasBuflineApi()
510
511     if !l:has_bufline_api && a:buffer isnot bufnr('')
512         return
513     endif
514
515     " If the file is in DOS mode, we have to remove carriage returns from
516     " the ends of lines before calling setline(), or we will see them
517     " twice.
518     let l:new_lines = getbufvar(a:buffer, '&fileformat') is# 'dos'
519     \   ? map(copy(a:lines), 'substitute(v:val, ''\r\+$'', '''', '''')')
520     \   : a:lines
521     let l:first_line_to_remove = len(l:new_lines) + 1
522
523     " Use a Vim API for setting lines in other buffers, if available.
524     if l:has_bufline_api
525         if has('nvim')
526             " save and restore signs to avoid flickering
527             let signs = sign_getplaced(a:buffer, {'group': 'ale'})[0].signs
528
529             call nvim_buf_set_lines(a:buffer, 0, l:first_line_to_remove, 0, l:new_lines)
530
531             " restore signs (invalid line numbers will be skipped)
532             call sign_placelist(map(signs, {_, v -> extend(v, {'buffer': a:buffer})}))
533         else
534             call setbufline(a:buffer, 1, l:new_lines)
535         endif
536
537         call deletebufline(a:buffer, l:first_line_to_remove, '$')
538     " Fall back on setting lines the old way, for the current buffer.
539     else
540         let l:old_line_length = line('$')
541
542         if l:old_line_length >= l:first_line_to_remove
543             let l:save = winsaveview()
544             silent execute
545             \   l:first_line_to_remove . ',' . l:old_line_length . 'd_'
546             call winrestview(l:save)
547         endif
548
549         call setline(1, l:new_lines)
550     endif
551
552     return l:new_lines
553 endfunction
554
555 function! ale#util#GetBufferContents(buffer) abort
556     return join(getbufline(a:buffer, 1, '$'), "\n") . "\n"
557 endfunction
558
559 function! ale#util#ToURI(resource) abort
560     let l:uri_handler = ale#uri#GetURIHandler(a:resource)
561
562     if l:uri_handler is# v:null
563         " resource is a filesystem path
564         let l:uri = ale#path#ToFileURI(a:resource)
565     else
566         " resource is a URI
567         let l:uri = a:resource
568     endif
569
570     return l:uri
571 endfunction
572
573 function! ale#util#ToResource(uri) abort
574     let l:uri_handler = ale#uri#GetURIHandler(a:uri)
575
576     if l:uri_handler is# v:null
577         " resource is a filesystem path
578         let l:resource = ale#path#FromFileURI(a:uri)
579     else
580         " resource is a URI
581         let l:resource = a:uri
582     endif
583
584     return l:resource
585 endfunction