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: Backend execution and job management
3 " Executes linters in the background, using NeoVim or Vim 8 jobs
5 " Remapping of linter problems.
6 let g:ale_type_map = get(g:, 'ale_type_map', {})
7 let g:ale_filename_mappings = get(g:, 'ale_filename_mappings', {})
9 if !has_key(s:, 'executable_cache_map')
10 let s:executable_cache_map = {}
13 function! ale#engine#CleanupEveryBuffer() abort
14 for l:key in keys(g:ale_buffer_info)
15 " The key could be a filename or a buffer number, so try and
16 " convert it to a number. We need a number for the other
18 let l:buffer = str2nr(l:key)
21 " Stop all jobs and clear the results for everything, and delete
22 " all of the data we stored for the buffer.
23 call ale#engine#Cleanup(l:buffer)
28 function! ale#engine#MarkLinterActive(info, linter) abort
31 for l:other_linter in a:info.active_linter_list
32 if l:other_linter.name is# a:linter.name
39 call add(a:info.active_linter_list, a:linter)
43 function! ale#engine#MarkLinterInactive(info, linter_name) abort
44 call filter(a:info.active_linter_list, 'v:val.name isnot# a:linter_name')
47 function! ale#engine#ResetExecutableCache() abort
48 let s:executable_cache_map = {}
51 " Check if files are executable, and if they are, remember that they are
52 " for subsequent calls. We'll keep checking until programs can be executed.
53 function! ale#engine#IsExecutable(buffer, executable) abort
54 if empty(a:executable)
55 " Don't log the executable check if the executable string is empty.
59 " Check for a cached executable() check.
60 let l:result = get(s:executable_cache_map, a:executable, v:null)
62 if l:result isnot v:null
66 " Check if the file is executable, and convert -1 to 1.
67 let l:result = executable(a:executable) isnot 0
69 " Cache the executable check if we found it, or if the option to cache
70 " failing checks is on.
71 if l:result || get(g:, 'ale_cache_executable_check_failures')
72 let s:executable_cache_map[a:executable] = l:result
75 if g:ale_history_enabled
76 call ale#history#Add(a:buffer, l:result, 'executable', a:executable)
82 function! ale#engine#InitBufferInfo(buffer) abort
83 if !has_key(g:ale_buffer_info, a:buffer)
84 " active_linter_list will hold the list of active linter names
85 " loclist holds the loclist items after all jobs have completed.
86 let g:ale_buffer_info[a:buffer] = {
87 \ 'active_linter_list': [],
88 \ 'active_other_sources_list': [],
98 " This function is documented and part of the public API.
100 " Return 1 if ALE is busy checking a given buffer
101 function! ale#engine#IsCheckingBuffer(buffer) abort
102 let l:info = get(g:ale_buffer_info, a:buffer, {})
104 return !empty(get(l:info, 'active_linter_list', []))
105 \ || !empty(get(l:info, 'active_other_sources_list', []))
108 function! ale#engine#HandleLoclist(linter_name, buffer, loclist, from_other_source) abort
109 let l:info = get(g:ale_buffer_info, a:buffer, {})
115 if !a:from_other_source
116 " Remove this linter from the list of active linters.
117 " This may have already been done when the job exits.
118 call filter(l:info.active_linter_list, 'v:val.name isnot# a:linter_name')
121 " Make some adjustments to the loclists to fix common problems, and also
122 " to set default values for loclist items.
123 let l:linter_loclist = ale#engine#FixLocList(
126 \ a:from_other_source,
130 " Remove previous items for this linter.
131 call filter(l:info.loclist, 'v:val.linter_name isnot# a:linter_name')
133 " We don't need to add items or sort the list when this list is empty.
134 if !empty(l:linter_loclist)
136 call extend(l:info.loclist, l:linter_loclist)
138 " Sort the loclist again.
139 " We need a sorted list so we can run a binary search against it
140 " for efficient lookup of the messages in the cursor handler.
141 call sort(l:info.loclist, 'ale#util#LocItemCompare')
144 if ale#ShouldDoNothing(a:buffer)
148 call ale#engine#SetResults(a:buffer, l:info.loclist)
151 function! s:HandleExit(job_info, buffer, output, data) abort
152 let l:buffer_info = get(g:ale_buffer_info, a:buffer)
154 if empty(l:buffer_info)
158 let l:linter = a:job_info.linter
159 let l:executable = a:job_info.executable
161 " Remove this job from the list.
162 call ale#engine#MarkLinterInactive(l:buffer_info, l:linter.name)
164 " Stop here if we land in the handle for a job completing if we're in
166 if ale#util#InSandbox()
170 if has('nvim') && !empty(a:output) && empty(a:output[-1])
171 call remove(a:output, -1)
175 let l:loclist = ale#util#GetFunction(l:linter.callback)(a:buffer, a:output)
176 " Handle the function being unknown, or being deleted.
181 if type(l:loclist) isnot# v:t_list
182 " we only expect the list type; don't pass anything else down to
183 " `ale#engine#HandleLoclist` since it won't understand it
187 call ale#engine#HandleLoclist(l:linter.name, a:buffer, l:loclist, 0)
190 function! ale#engine#SetResults(buffer, loclist) abort
191 let l:linting_is_done = !ale#engine#IsCheckingBuffer(a:buffer)
193 if g:ale_use_neovim_diagnostics_api
194 call ale#engine#SendResultsToNeovimDiagnostics(a:buffer, a:loclist)
197 " Set signs first. This could potentially fix some line numbers.
198 " The List could be sorted again here by SetSigns.
199 if !g:ale_use_neovim_diagnostics_api && g:ale_set_signs
200 call ale#sign#SetSigns(a:buffer, a:loclist)
203 if g:ale_set_quickfix || g:ale_set_loclist
204 call ale#list#SetLists(a:buffer, a:loclist)
207 if exists('*ale#statusline#Update')
208 " Don't load/run if not already loaded.
209 call ale#statusline#Update(a:buffer, a:loclist)
212 if !g:ale_use_neovim_diagnostics_api && g:ale_set_highlights
213 call ale#highlight#SetHighlights(a:buffer, a:loclist)
216 if !g:ale_use_neovim_diagnostics_api
217 \&& (g:ale_virtualtext_cursor is# 'all' || g:ale_virtualtext_cursor == 2)
218 call ale#virtualtext#SetTexts(a:buffer, a:loclist)
223 " Try and echo the warning now.
224 " This will only do something meaningful if we're in normal mode.
225 call ale#cursor#EchoCursorWarning()
228 if !g:ale_use_neovim_diagnostics_api
229 \&& (g:ale_virtualtext_cursor is# 'current' || g:ale_virtualtext_cursor == 1)
230 " Try and show the warning now.
231 " This will only do something meaningful if we're in normal mode.
232 call ale#virtualtext#ShowCursorWarning()
235 " Reset the save event marker, used for opening windows, etc.
236 call setbufvar(a:buffer, 'ale_save_event_fired', 0)
237 " Set a marker showing how many times a buffer has been checked.
241 \ getbufvar(a:buffer, 'ale_linted', 0) + 1
244 " Automatically remove all managed temporary files and directories
245 " now that all jobs have completed.
246 call ale#command#RemoveManagedFiles(a:buffer)
248 " Call user autocommands. This allows users to hook into ALE's lint cycle.
249 silent doautocmd <nomodeline> User ALELintPost
253 function! ale#engine#SendResultsToNeovimDiagnostics(buffer, loclist) abort
255 " We will warn the user on startup as well if they try to set
256 " g:ale_use_neovim_diagnostics_api outside of a Neovim context.
260 " Keep the Lua surface area really small in the VimL part of ALE,
261 " and just require the diagnostics.lua module on demand.
262 let l:SendDiagnostics = luaeval('require("ale.diagnostics").send')
263 call l:SendDiagnostics(a:buffer, a:loclist)
266 function! s:RemapItemTypes(type_map, loclist) abort
267 for l:item in a:loclist
268 let l:key = l:item.type
269 \ . (get(l:item, 'sub_type', '') is# 'style' ? 'S' : '')
270 let l:new_key = get(a:type_map, l:key, '')
273 \|| l:new_key is# 'ES'
274 \|| l:new_key is# 'W'
275 \|| l:new_key is# 'WS'
276 \|| l:new_key is# 'I'
277 let l:item.type = l:new_key[0]
279 if l:new_key is# 'ES' || l:new_key is# 'WS'
280 let l:item.sub_type = 'style'
281 elseif has_key(l:item, 'sub_type')
282 call remove(l:item, 'sub_type')
288 function! ale#engine#FixLocList(buffer, linter_name, from_other_source, loclist) abort
289 let l:mappings = ale#GetFilenameMappings(a:buffer, a:linter_name)
291 if !empty(l:mappings)
292 " We need to apply reverse filename mapping here.
293 let l:mappings = ale#filename_mapping#Invert(l:mappings)
297 let l:new_loclist = []
299 " Some errors have line numbers beyond the end of the file,
300 " so we need to adjust them so they set the error at the last line
301 " of the file instead.
302 let l:last_line_number = ale#util#GetLineCount(a:buffer)
304 for l:old_item in a:loclist
305 " Copy the loclist item with some default values and corrections.
307 " line and column numbers will be converted to numbers.
308 " The buffer will default to the buffer being checked.
309 " The vcol setting will default to 0, a byte index.
310 " The error type will default to 'E' for errors.
311 " The error number will default to -1.
313 " The line number and text are the only required keys.
315 " The linter_name will be set on the errors so it can be used in
316 " output, filtering, etc..
319 \ 'text': l:old_item.text,
320 \ 'lnum': str2nr(l:old_item.lnum),
321 \ 'col': str2nr(get(l:old_item, 'col', 0)),
323 \ 'type': get(l:old_item, 'type', 'E'),
324 \ 'nr': get(l:old_item, 'nr', -1),
325 \ 'linter_name': a:linter_name,
328 if a:from_other_source
329 let l:item.from_other_source = 1
332 if has_key(l:old_item, 'code')
333 let l:item.code = l:old_item.code
336 let l:old_name = get(l:old_item, 'filename', '')
338 " Map parsed from output to local filesystem files.
339 if !empty(l:old_name) && !empty(l:mappings)
340 let l:old_name = ale#filename_mapping#Map(l:old_name, l:mappings)
343 if !empty(l:old_name) && !ale#path#IsTempName(l:old_name)
344 " Use the filename given.
345 " Temporary files are assumed to be for this buffer,
346 " and the filename is not included then, because it looks bad
347 " in the loclist window.
348 let l:filename = l:old_name
349 let l:item.filename = l:filename
351 if has_key(l:old_item, 'bufnr')
352 " If a buffer number is also given, include that too.
353 " If Vim detects that he buffer number is valid, it will
354 " be used instead of the filename.
355 let l:item.bufnr = l:old_item.bufnr
356 elseif has_key(l:bufnr_map, l:filename)
357 " Get the buffer number from the map, which can be faster.
358 let l:item.bufnr = l:bufnr_map[l:filename]
360 " Look up the buffer number.
361 let l:item.bufnr = bufnr(l:filename)
362 let l:bufnr_map[l:filename] = l:item.bufnr
364 elseif has_key(l:old_item, 'bufnr')
365 let l:item.bufnr = l:old_item.bufnr
368 if has_key(l:old_item, 'detail')
369 let l:item.detail = l:old_item.detail
372 " Pass on a end_col key if set, used for highlights.
373 if has_key(l:old_item, 'end_col')
374 let l:item.end_col = str2nr(l:old_item.end_col)
377 if has_key(l:old_item, 'end_lnum')
378 let l:item.end_lnum = str2nr(l:old_item.end_lnum)
380 " When the error ends after the end of the file, put it at the
381 " end. This is only done for the current buffer.
382 if l:item.bufnr == a:buffer && l:item.end_lnum > l:last_line_number
383 let l:item.end_lnum = l:last_line_number
387 if has_key(l:old_item, 'sub_type')
388 let l:item.sub_type = l:old_item.sub_type
392 " When errors appear before line 1, put them at line 1.
394 elseif l:item.bufnr == a:buffer && l:item.lnum > l:last_line_number
395 " When errors go beyond the end of the file, put them at the end.
396 " This is only done for the current buffer.
397 let l:item.lnum = l:last_line_number
398 elseif get(l:old_item, 'vcol', 0)
399 " Convert virtual column positions to byte positions.
400 " The positions will be off if the buffer has changed recently.
401 let l:line = getbufline(a:buffer, l:item.lnum)[0]
403 let l:item.col = ale#util#Col(l:line, l:item.col)
405 if has_key(l:item, 'end_col')
406 let l:end_line = get(l:item, 'end_lnum', l:line) != l:line
407 \ ? getbufline(a:buffer, l:item.end_lnum)[0]
410 let l:item.end_col = ale#util#Col(l:end_line, l:item.end_col)
414 call add(l:new_loclist, l:item)
417 let l:type_map = get(ale#Var(a:buffer, 'type_map'), a:linter_name, {})
419 if !empty(l:type_map)
420 call s:RemapItemTypes(l:type_map, l:new_loclist)
426 " Given part of a command, replace any % with %%, so that no characters in
427 " the string will be replaced with filenames, etc.
428 function! ale#engine#EscapeCommandPart(command_part) abort
429 " TODO: Emit deprecation warning here later.
430 return ale#command#EscapeCommandPart(a:command_part)
435 " Returns 1 when a job was started successfully.
436 function! s:RunJob(command, options) abort
437 if ale#command#IsDeferred(a:command)
438 let a:command.result_callback = {
439 \ command -> s:RunJob(command, a:options)
445 let l:command = a:command
451 let l:cwd = a:options.cwd
452 let l:executable = a:options.executable
453 let l:buffer = a:options.buffer
454 let l:linter = a:options.linter
455 let l:output_stream = a:options.output_stream
456 let l:read_buffer = a:options.read_buffer && !a:options.lint_file
457 let l:info = g:ale_buffer_info[l:buffer]
459 let l:Callback = function('s:HandleExit', [{
460 \ 'linter': l:linter,
461 \ 'executable': l:executable,
463 let l:result = ale#command#Run(l:buffer, l:command, l:Callback, {
465 \ 'output_stream': l:output_stream,
466 \ 'executable': l:executable,
467 \ 'read_buffer': l:read_buffer,
469 \ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:linter.name),
472 " Only proceed if the job is being run.
477 call ale#engine#MarkLinterActive(l:info, l:linter)
479 silent doautocmd <nomodeline> User ALEJobStarted
484 function! s:StopCurrentJobs(buffer, clear_lint_file_jobs, linter_slots) abort
485 let l:info = get(g:ale_buffer_info, a:buffer, {})
486 call ale#command#StopJobs(a:buffer, 'linter')
488 " Update the active linter list, clearing out anything not running.
489 if a:clear_lint_file_jobs
490 call ale#command#StopJobs(a:buffer, 'file_linter')
491 let l:info.active_linter_list = []
493 let l:lint_file_map = {}
495 " Use a previously computed map of `lint_file` values to find
496 " linters that are used for linting files.
497 for [l:lint_file, l:linter] in a:linter_slots
499 let l:lint_file_map[l:linter.name] = 1
503 " Keep jobs for linting files when we're only linting buffers.
504 call filter(l:info.active_linter_list, 'get(l:lint_file_map, v:val.name)')
508 function! ale#engine#Stop(buffer) abort
509 call s:StopCurrentJobs(a:buffer, 1, [])
512 function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort
513 " Figure out which linters are still enabled, and remove
514 " problems for linters which are no longer enabled.
515 " Problems from other sources will be kept.
518 for l:linter in a:linters
519 let l:name_map[l:linter.name] = 1
523 \ get(g:ale_buffer_info[a:buffer], 'loclist', []),
524 \ 'get(v:val, ''from_other_source'') || get(l:name_map, get(v:val, ''linter_name''))',
528 function! s:AddProblemsFromOtherBuffers(buffer, linters) abort
529 let l:filename = expand('#' . a:buffer . ':p')
533 " Build a map of the active linters.
534 for l:linter in a:linters
535 let l:name_map[l:linter.name] = 1
538 " Find the items from other buffers, for the linters that are enabled.
539 for l:info in values(g:ale_buffer_info)
540 for l:item in l:info.loclist
541 if has_key(l:item, 'filename')
542 \&& l:item.filename is# l:filename
543 \&& has_key(l:name_map, l:item.linter_name)
544 " Copy the items and set the buffer numbers to this one.
545 let l:new_item = copy(l:item)
546 let l:new_item.bufnr = a:buffer
547 call add(l:loclist, l:new_item)
553 call sort(l:loclist, function('ale#util#LocItemCompareWithText'))
554 call uniq(l:loclist, function('ale#util#LocItemCompareWithText'))
556 " Set the loclist variable, used by some parts of ALE.
557 let g:ale_buffer_info[a:buffer].loclist = l:loclist
558 call ale#engine#SetResults(a:buffer, l:loclist)
562 function! s:RunIfExecutable(buffer, linter, lint_file, executable) abort
563 if ale#command#IsDeferred(a:executable)
564 let a:executable.result_callback = {
565 \ executable -> s:RunIfExecutable(
576 if ale#engine#IsExecutable(a:buffer, a:executable)
577 " Use different job types for file or linter jobs.
578 let l:job_type = a:lint_file ? 'file_linter' : 'linter'
579 call setbufvar(a:buffer, 'ale_job_type', l:job_type)
581 " Get the cwd for the linter and set it before we call GetCommand.
582 " This will ensure that ale#command#Run uses it by default.
583 let l:cwd = ale#linter#GetCwd(a:buffer, a:linter)
585 if l:cwd isnot v:null
586 call ale#command#SetCwd(a:buffer, l:cwd)
589 let l:command = ale#linter#GetCommand(a:buffer, a:linter)
591 if l:cwd isnot v:null
592 call ale#command#ResetCwd(a:buffer)
597 \ 'executable': a:executable,
598 \ 'buffer': a:buffer,
599 \ 'linter': a:linter,
600 \ 'output_stream': get(a:linter, 'output_stream', 'stdout'),
601 \ 'read_buffer': a:linter.read_buffer,
602 \ 'lint_file': a:lint_file,
605 return s:RunJob(l:command, l:options)
611 " Run a linter for a buffer.
613 " Returns 1 if the linter was successfully run.
614 function! s:RunLinter(buffer, linter, lint_file) abort
615 if !empty(a:linter.lsp)
616 return ale#lsp_linter#CheckWithLSP(a:buffer, a:linter)
618 let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
620 return s:RunIfExecutable(a:buffer, a:linter, a:lint_file, l:executable)
626 function! s:GetLintFileSlots(buffer, linters) abort
627 let l:linter_slots = []
629 for l:linter in a:linters
630 let l:LintFile = l:linter.lint_file
632 if type(l:LintFile) is v:t_func
633 let l:LintFile = l:LintFile(a:buffer)
636 call add(l:linter_slots, [l:LintFile, l:linter])
639 return l:linter_slots
642 function! s:GetLintFileValues(slots, Callback) abort
643 let l:deferred_list = []
646 for [l:lint_file, l:linter] in a:slots
647 while ale#command#IsDeferred(l:lint_file) && has_key(l:lint_file, 'value')
648 " If we've already computed the return value, use it.
649 let l:lint_file = l:lint_file.value
652 if ale#command#IsDeferred(l:lint_file)
653 " If we are going to return the result later, wait for it.
654 call add(l:deferred_list, l:lint_file)
656 " If we have the value now, coerce it to 0 or 1.
657 let l:lint_file = l:lint_file is 1
660 call add(l:new_slots, [l:lint_file, l:linter])
663 if !empty(l:deferred_list)
664 for l:deferred in l:deferred_list
665 let l:deferred.result_callback =
666 \ {-> s:GetLintFileValues(l:new_slots, a:Callback)}
669 call a:Callback(l:new_slots)
673 function! s:RunLinters(
680 call s:StopCurrentJobs(a:buffer, a:should_lint_file, a:slots)
681 call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters)
683 " We can only clear the results if we aren't checking the buffer.
684 let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer)
686 silent doautocmd <nomodeline> User ALELintPre
688 for [l:lint_file, l:linter] in a:slots
689 " Only run lint_file linters if we should.
690 if !l:lint_file || a:should_lint_file
691 if s:RunLinter(a:buffer, l:linter, l:lint_file)
692 " If a single linter ran, we shouldn't clear everything.
693 let l:can_clear_results = 0
696 " If we skipped running a lint_file linter still in the list,
697 " we shouldn't clear everything.
698 let l:can_clear_results = 0
702 " Clear the results if we can. This needs to be done when linters are
703 " disabled, or ALE itself is disabled.
704 if l:can_clear_results
705 call ale#engine#SetResults(a:buffer, [])
707 call s:AddProblemsFromOtherBuffers(
709 \ map(copy(a:slots), 'v:val[1]')
714 function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort
715 " Initialise the buffer information if needed.
716 let l:new_buffer = ale#engine#InitBufferInfo(a:buffer)
718 call s:GetLintFileValues(
719 \ s:GetLintFileSlots(a:buffer, a:linters),
721 \ slots -> s:RunLinters(
725 \ a:should_lint_file,
734 " This function will stop all current jobs for the buffer,
735 " clear the state of everything, and remove the Dictionary for managing
737 function! ale#engine#Cleanup(buffer) abort
738 " Don't bother with cleanup code when newer NeoVim versions are exiting.
739 if get(v:, 'exiting', v:null) isnot v:null
743 if exists('*ale#lsp#CloseDocument')
744 call ale#lsp#CloseDocument(a:buffer)
747 if !has_key(g:ale_buffer_info, a:buffer)
751 call ale#engine#RunLinters(a:buffer, [], 1)
753 call remove(g:ale_buffer_info, a:buffer)
756 " Given a buffer number, return the warnings and errors for a given buffer.
757 function! ale#engine#GetLoclist(buffer) abort
758 if !has_key(g:ale_buffer_info, a:buffer)
762 return g:ale_buffer_info[a:buffer].loclist