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.
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Contains miscellaneous functions
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)
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)
14 " Show a message in as small a window as possible.
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, {})
23 call ale#preview#CloseIfTypeMatches('ale-preview.message')
26 " We have to assume the user is using a monospace font.
27 if has('nvim') || (a:string !~? "\n" && len(a:string) < &columns)
31 call ale#preview#Show(split(a:string, "\n"), extend(
33 \ 'filetype': 'ale-preview.message',
41 " A wrapper function for execute, so we can test executing some commands.
42 function! ale#util#Execute(expr) abort
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'
51 let g:ale#util#nul_file = 'nul'
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.
59 " Note that jobs and IDs are the same thing on NeoVim.
60 function! ale#util#JoinNeovimOutput(job, last_line, data, mode, callback) abort
62 call a:callback(a:job, join(a:data, "\n"))
67 let l:lines = a:data[:-2]
70 let l:lines[0] = a:last_line . l:lines[0]
71 let l:new_last_line = a:data[-1]
73 let l:new_last_line = a:last_line . get(a:data, 0, '')
77 call a:callback(a:job, l:line)
80 return l:new_last_line
83 " Return the number of lines for a given buffer.
84 function! ale#util#GetLineCount(buffer) abort
85 return len(getbufline(a:buffer, 1, '$'))
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)
93 return a:string_or_ref
96 " Open the file (at the given line).
97 " options['open_in'] can be:
98 " current-buffer (default)
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)
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)
119 call cursor(a:line, a:column)
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
129 function! ale#util#GetItemPriority(item) abort
130 if a:item.type is# 'I'
131 return g:ale#util#info_priority
134 if a:item.type is# 'W'
135 if get(a:item, 'sub_type', '') is# 'style'
136 return g:ale#util#style_warning_priority
139 return g:ale#util#warning_priority
142 if get(a:item, 'sub_type', '') is# 'style'
143 return g:ale#util#style_error_priority
146 return g:ale#util#error_priority
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
156 if a:left.bufnr > a:right.bufnr
160 if a:left.bufnr == -1
161 if a:left.filename < a:right.filename
165 if a:left.filename > a:right.filename
170 if a:left.lnum < a:right.lnum
174 if a:left.lnum > a:right.lnum
178 if a:left.col < a:right.col
182 if a:left.col > a:right.col
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')
192 let l:left_priority = ale#util#GetItemPriority(a:left)
193 let l:right_priority = ale#util#GetItemPriority(a:right)
195 if l:left_priority < l:right_priority
199 if l:left_priority > l:right_priority
206 " Compare two loclist items, including the text for the items.
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)
216 if a:left.text < a:right.text
220 if a:left.text > a:right.text
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.
231 " -1 will be returned if nothing can be found.
232 function! ale#util#BinarySearch(loclist, buffer, line, column) abort
234 let l:max = len(a:loclist) - 1
241 let l:mid = (l:min + l:max) / 2
242 let l:item = a:loclist[l:mid]
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
254 " This part is a small sequential search.
257 " Search backwards to find the first problem on the line.
259 \&& a:loclist[l:index - 1].bufnr == a:buffer
260 \&& a:loclist[l:index - 1].lnum == a:line
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
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
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
288 " A function for testing if a function is running inside a sandbox.
290 function! ale#util#InSandbox() abort
292 let &l:equalprg=&l:equalprg
294 " E48 is the sandbox error.
301 function! ale#util#Tempname() abort
302 let l:clear_tempdir = 0
304 if exists('$TMPDIR') && empty($TMPDIR)
305 let l:clear_tempdir = 1
310 let l:name = tempname() " no-custom-checks
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().
324 " Only the first pattern which matches a line will be returned.
325 function! ale#util#GetMatches(lines, patterns) abort
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]
330 for l:line in l:lines
331 for l:pattern in l:patterns
332 let l:match = matchlist(l:line, l:pattern)
335 call add(l:matches, l:match)
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,
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)')
354 function! s:LoadArgCount(function) abort
356 let l:output = execute('function a:function')
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# ''...''')
364 return len(l:arg_list)
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)
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.
377 let l:function_name = matchlist(string(l:Function), 'function([''"]\(.\+\)[''"])')[1]
379 if l:function_name =~# '#'
380 execute 'runtime autoload/' . join(split(l:function_name, '#')[:-2], '/') . '.vim'
381 let l:count = s:LoadArgCount(l:Function)
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')
394 " Escape a string so that it can be used as a literal string inside an evaled
396 function! ale#util#EscapeVim(unsafe_string) abort
397 return "'" . substitute(a:unsafe_string, "'", "''", 'g') . "'"
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.
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
412 let l:str = type(a:data) is v:t_string ? a:data : join(a:data, '')
415 let l:result = json_decode(l:str)
417 " Vim 8 only uses the value v:none for decoding blank strings.
418 if !has('nvim') && l:result is v:none
428 " Write a file, including carriage return characters for DOS files.
430 " The buffer number is required for determining the fileformat setting for
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'', '''')')
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'
440 call writefile(l:corrected_lines, a:filename, l:flags) " no-custom-checks
443 if !exists('s:patial_timers')
444 let s:partial_timers = {}
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)
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.
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]
467 function! ale#util#StopPartialTimer(timer_id) abort
468 call timer_stop(a:timer_id)
470 if has_key(s:partial_timers, a:timer_id)
471 call remove(s:partial_timers, a:timer_id)
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
482 return strlen(join(split(a:str, '\zs')[0:a:chr - 2], '')) + 1
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] : {}
492 return [l:info, l:loc]
495 function! ale#util#Input(message, value, ...) abort
497 return input(a:message, a:value, a:1)
499 return input(a:message, a:value)
503 function! ale#util#HasBuflineApi() abort
504 return exists('*deletebufline') && exists('*setbufline')
507 " Sets buffer contents to lines
508 function! ale#util#SetBufferContents(buffer, lines) abort
509 let l:has_bufline_api = ale#util#HasBuflineApi()
511 if !l:has_bufline_api && a:buffer isnot bufnr('')
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
518 let l:new_lines = getbufvar(a:buffer, '&fileformat') is# 'dos'
519 \ ? map(copy(a:lines), 'substitute(v:val, ''\r\+$'', '''', '''')')
521 let l:first_line_to_remove = len(l:new_lines) + 1
523 " Use a Vim API for setting lines in other buffers, if available.
526 " save and restore signs to avoid flickering
527 let signs = sign_getplaced(a:buffer, {'group': 'ale'})[0].signs
529 call nvim_buf_set_lines(a:buffer, 0, l:first_line_to_remove, 0, l:new_lines)
531 " restore signs (invalid line numbers will be skipped)
532 call sign_placelist(map(signs, {_, v -> extend(v, {'buffer': a:buffer})}))
534 call setbufline(a:buffer, 1, l:new_lines)
537 call deletebufline(a:buffer, l:first_line_to_remove, '$')
538 " Fall back on setting lines the old way, for the current buffer.
540 let l:old_line_length = line('$')
542 if l:old_line_length >= l:first_line_to_remove
543 let l:save = winsaveview()
545 \ l:first_line_to_remove . ',' . l:old_line_length . 'd_'
546 call winrestview(l:save)
549 call setline(1, l:new_lines)
555 function! ale#util#GetBufferContents(buffer) abort
556 return join(getbufline(a:buffer, 1, '$'), "\n") . "\n"
559 function! ale#util#ToURI(resource) abort
560 let l:uri_handler = ale#uri#GetURIHandler(a:resource)
562 if l:uri_handler is# v:null
563 " resource is a filesystem path
564 let l:uri = ale#path#ToFileURI(a:resource)
567 let l:uri = a:resource
573 function! ale#util#ToResource(uri) abort
574 let l:uri_handler = ale#uri#GetURIHandler(a:uri)
576 if l:uri_handler is# v:null
577 " resource is a filesystem path
578 let l:resource = ale#path#FromFileURI(a:uri)
581 let l:resource = a:uri