]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/command.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 / command.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Functions for formatting command strings, running commands, and
3 "   managing files during linting and fixing cycles.
4
5 " This dictionary holds lists of files and directories to remove later.
6 if !exists('s:buffer_data')
7     let s:buffer_data = {}
8 endif
9
10 " The regular expression used for formatting filenames with modifiers.
11 let s:path_format_regex = '\v\%s(%(:h|:t|:r|:e)*)'
12
13 " Used to get the data in tests.
14 function! ale#command#GetData() abort
15     return deepcopy(s:buffer_data)
16 endfunction
17
18 function! ale#command#ClearData() abort
19     let s:buffer_data = {}
20 endfunction
21
22 function! ale#command#InitData(buffer) abort
23     if !has_key(s:buffer_data, a:buffer)
24         let s:buffer_data[a:buffer] = {
25         \   'jobs': {},
26         \   'file_list': [],
27         \   'directory_list': [],
28         \}
29     endif
30 endfunction
31
32 " Set the cwd for commands that are about to run.
33 " Used internally.
34 function! ale#command#SetCwd(buffer, cwd) abort
35     call ale#command#InitData(a:buffer)
36     let s:buffer_data[a:buffer].cwd = a:cwd
37 endfunction
38
39 function! ale#command#ResetCwd(buffer) abort
40     if has_key(s:buffer_data, a:buffer)
41         let s:buffer_data[a:buffer].cwd = v:null
42     endif
43 endfunction
44
45 function! ale#command#ManageFile(buffer, file) abort
46     call ale#command#InitData(a:buffer)
47     call add(s:buffer_data[a:buffer].file_list, a:file)
48 endfunction
49
50 function! ale#command#ManageDirectory(buffer, directory) abort
51     call ale#command#InitData(a:buffer)
52     call add(s:buffer_data[a:buffer].directory_list, a:directory)
53 endfunction
54
55 function! ale#command#CreateFile(buffer) abort
56     " This variable can be set to 1 in tests to stub this out.
57     if get(g:, 'ale_create_dummy_temporary_file')
58         return 'TEMP'
59     endif
60
61     let l:temporary_file = ale#util#Tempname()
62     call ale#command#ManageFile(a:buffer, l:temporary_file)
63
64     return l:temporary_file
65 endfunction
66
67 " Create a new temporary directory and manage it in one go.
68 function! ale#command#CreateDirectory(buffer) abort
69     " This variable can be set to 1 in tests to stub this out.
70     if get(g:, 'ale_create_dummy_temporary_file')
71         return 'TEMP_DIR'
72     endif
73
74     let l:temporary_directory = ale#util#Tempname()
75     " Create the temporary directory for the file, unreadable by 'other'
76     " users.
77     call mkdir(l:temporary_directory, '', 0750)
78     call ale#command#ManageDirectory(a:buffer, l:temporary_directory)
79
80     return l:temporary_directory
81 endfunction
82
83 function! ale#command#RemoveManagedFiles(buffer) abort
84     let l:info = get(s:buffer_data, a:buffer, {})
85
86     if !empty(l:info) && empty(l:info.jobs)
87         " We can't delete anything in a sandbox, so wait until we escape from
88         " it to delete temporary files and directories.
89         if ale#util#InSandbox()
90             return
91         endif
92
93         " Delete files with a call akin to a plan `rm` command.
94         for l:filename in l:info.file_list
95             call delete(l:filename)
96         endfor
97
98         " Delete directories like `rm -rf`.
99         " Directories are handled differently from files, so paths that are
100         " intended to be single files can be set up for automatic deletion
101         " without accidentally deleting entire directories.
102         for l:directory in l:info.directory_list
103             call delete(l:directory, 'rf')
104         endfor
105
106         call remove(s:buffer_data, a:buffer)
107     endif
108 endfunction
109
110 function! ale#command#CreateTempFile(buffer, temporary_file, input) abort
111     if empty(a:temporary_file)
112         " There is no file, so we didn't create anything.
113         return 0
114     endif
115
116     " Use an existing list of lines of input if we have it, or get the lines
117     " from the file.
118     let l:lines = a:input isnot v:null ? a:input : getbufline(a:buffer, 1, '$')
119
120     let l:temporary_directory = fnamemodify(a:temporary_file, ':h')
121     " Create the temporary directory for the file, unreadable by 'other'
122     " users.
123     call mkdir(l:temporary_directory, '', 0750)
124     " Automatically delete the directory later.
125     call ale#command#ManageDirectory(a:buffer, l:temporary_directory)
126     " Write the buffer out to a file.
127     call ale#util#Writefile(a:buffer, l:lines, a:temporary_file)
128
129     return 1
130 endfunction
131
132 function! s:TemporaryFilename(buffer) abort
133     let l:filename = fnamemodify(bufname(a:buffer), ':t')
134
135     if empty(l:filename)
136         " If the buffer's filename is empty, create a dummy filename.
137         let l:ft = getbufvar(a:buffer, '&filetype')
138         let l:filename = 'file' . ale#filetypes#GuessExtension(l:ft)
139     endif
140
141     " Create a temporary filename, <temp_dir>/<original_basename>
142     " The file itself will not be created by this function.
143     return ale#util#Tempname() . (has('win32') ? '\' : '/') . l:filename
144 endfunction
145
146 " Given part of a command, replace any % with %%, so that no characters in
147 " the string will be replaced with filenames, etc.
148 function! ale#command#EscapeCommandPart(command_part) abort
149     return substitute(a:command_part, '%', '%%', 'g')
150 endfunction
151
152 " Format a filename, converting it with filename mappings, if non-empty,
153 " and escaping it for putting into a command string.
154 "
155 " The filename can be modified.
156 function! s:FormatFilename(filename, mappings, modifiers) abort
157     let l:filename = a:filename
158
159     if !empty(a:mappings)
160         let l:filename = ale#filename_mapping#Map(l:filename, a:mappings)
161     endif
162
163     if !empty(a:modifiers)
164         let l:filename = fnamemodify(l:filename, a:modifiers)
165     endif
166
167     return ale#Escape(l:filename)
168 endfunction
169
170 " Produce a command prefix to check to a particular directory for a command.
171 " %s format markers with filename-modifiers can be used as the directory, and
172 " will be returned verbatim for formatting in paths relative to files.
173 function! ale#command#CdString(directory) abort
174     let l:match = matchstrpos(a:directory, s:path_format_regex)
175     " Do not escape the directory here if it's a valid format string.
176     " This allows us to use sequences like %s:h, %s:h:h, etc.
177     let l:directory = l:match[1:] == [0, len(a:directory)]
178     \   ? a:directory
179     \   : ale#Escape(a:directory)
180
181     if has('win32')
182         return 'cd /d ' . l:directory . ' && '
183     endif
184
185     return 'cd ' . l:directory . ' && '
186 endfunction
187
188 " Given a command string, replace every...
189 " %s -> with the current filename
190 " %t -> with the name of an unused file in a temporary directory
191 " %% -> with a literal %
192 function! ale#command#FormatCommand(
193 \   buffer,
194 \   executable,
195 \   command,
196 \   pipe_file_if_needed,
197 \   input,
198 \   cwd,
199 \   mappings,
200 \) abort
201     let l:temporary_file = ''
202     let l:command = a:command
203
204     if !empty(a:cwd)
205         let l:command = ale#command#CdString(a:cwd) . l:command
206     endif
207
208     " First replace all uses of %%, used for literal percent characters,
209     " with an ugly string.
210     let l:command = substitute(l:command, '%%', '<<PERCENTS>>', 'g')
211
212     " Replace %e with the escaped executable, if available.
213     if !empty(a:executable) && l:command =~# '%e'
214         let l:command = substitute(l:command, '%e', '\=ale#Escape(a:executable)', 'g')
215     endif
216
217     " Replace all %s occurrences in the string with the name of the current
218     " file.
219     if l:command =~# '%s'
220         let l:filename = fnamemodify(bufname(a:buffer), ':p')
221         let l:command = substitute(
222         \   l:command,
223         \   s:path_format_regex,
224         \   '\=s:FormatFilename(l:filename, a:mappings, submatch(1))',
225         \   'g'
226         \)
227     endif
228
229     if a:input isnot v:false && l:command =~# '%t'
230         " Create a temporary filename, <temp_dir>/<original_basename>
231         " The file itself will not be created by this function.
232         let l:temporary_file = s:TemporaryFilename(a:buffer)
233         let l:command = substitute(
234         \   l:command,
235         \   '\v\%t(%(:h|:t|:r|:e)*)',
236         \   '\=s:FormatFilename(l:temporary_file, a:mappings, submatch(1))',
237         \   'g'
238         \)
239     endif
240
241     " Finish formatting so %% becomes %.
242     let l:command = substitute(l:command, '<<PERCENTS>>', '%', 'g')
243
244     if a:pipe_file_if_needed && empty(l:temporary_file)
245         " If we are to send the Vim buffer to a command, we'll do it
246         " in the shell. We'll write out the file to a temporary file,
247         " and then read it back in, in the shell.
248         let l:temporary_file = s:TemporaryFilename(a:buffer)
249         let l:command = l:command . ' < ' . ale#Escape(l:temporary_file)
250     endif
251
252     let l:file_created = ale#command#CreateTempFile(
253     \   a:buffer,
254     \   l:temporary_file,
255     \   a:input,
256     \)
257
258     return [l:temporary_file, l:command, l:file_created]
259 endfunction
260
261 function! ale#command#StopJobs(buffer, job_type) abort
262     let l:info = get(s:buffer_data, a:buffer, {})
263
264     if !empty(l:info)
265         let l:new_map = {}
266
267         for [l:job_id, l:job_type] in items(l:info.jobs)
268             let l:job_id = str2nr(l:job_id)
269
270             if a:job_type is# 'all' || a:job_type is# l:job_type
271                 call ale#job#Stop(l:job_id)
272             else
273                 let l:new_map[l:job_id] = l:job_type
274             endif
275         endfor
276
277         let l:info.jobs = l:new_map
278     endif
279 endfunction
280
281 function! s:GatherOutput(line_list, job_id, line) abort
282     call add(a:line_list, a:line)
283 endfunction
284
285 function! s:ExitCallback(buffer, line_list, Callback, data) abort
286     if !has_key(s:buffer_data, a:buffer)
287         return
288     endif
289
290     let l:jobs = s:buffer_data[a:buffer].jobs
291
292     if !has_key(l:jobs, a:data.job_id)
293         return
294     endif
295
296     let l:job_type = remove(l:jobs, a:data.job_id)
297
298     if g:ale_history_enabled
299         call ale#history#SetExitCode(a:buffer, a:data.job_id, a:data.exit_code)
300
301         " Log the output of the command for ALEInfo if we should.
302         if g:ale_history_log_output && a:data.log_output is 1
303             call ale#history#RememberOutput(
304             \   a:buffer,
305             \   a:data.job_id,
306             \   a:line_list[:]
307             \)
308         endif
309     endif
310
311     " If the callback starts any new jobs, use the same job type for them.
312     call setbufvar(a:buffer, 'ale_job_type', l:job_type)
313     let l:value = a:Callback(a:buffer, a:line_list, {
314     \   'exit_code': a:data.exit_code,
315     \   'temporary_file': a:data.temporary_file,
316     \})
317
318     let l:result = a:data.result
319     let l:result.value = l:value
320
321     " Set the default cwd for this buffer in this call stack.
322     call ale#command#SetCwd(a:buffer, l:result.cwd)
323
324     try
325         if get(l:result, 'result_callback', v:null) isnot v:null
326             call call(l:result.result_callback, [l:value])
327         endif
328     finally
329         call ale#command#ResetCwd(a:buffer)
330     endtry
331 endfunction
332
333 function! ale#command#Run(buffer, command, Callback, ...) abort
334     let l:options = get(a:000, 0, {})
335
336     if len(a:000) > 1
337         throw 'Too many arguments!'
338     endif
339
340     let l:output_stream = get(l:options, 'output_stream', 'stdout')
341     let l:line_list = []
342     let l:cwd = get(l:options, 'cwd', v:null)
343
344     if l:cwd is v:null
345         " Default the working directory to whatever it was for the last
346         " command run in the chain.
347         let l:cwd = get(get(s:buffer_data, a:buffer, {}), 'cwd', v:null)
348     endif
349
350     let [l:temporary_file, l:command, l:file_created] = ale#command#FormatCommand(
351     \   a:buffer,
352     \   get(l:options, 'executable', ''),
353     \   a:command,
354     \   get(l:options, 'read_buffer', 0),
355     \   get(l:options, 'input', v:null),
356     \   l:cwd,
357     \   get(l:options, 'filename_mappings', []),
358     \)
359     let l:command = ale#job#PrepareCommand(a:buffer, l:command)
360     let l:job_options = {
361     \   'exit_cb': {job_id, exit_code -> s:ExitCallback(
362     \       a:buffer,
363     \       l:line_list,
364     \       a:Callback,
365     \       {
366     \           'job_id': job_id,
367     \           'exit_code': exit_code,
368     \           'temporary_file': l:temporary_file,
369     \           'log_output': get(l:options, 'log_output', 1),
370     \           'result': l:result,
371     \       }
372     \   )},
373     \   'mode': 'nl',
374     \}
375
376     if l:output_stream is# 'stdout'
377         let l:job_options.out_cb = function('s:GatherOutput', [l:line_list])
378     elseif l:output_stream is# 'stderr'
379         let l:job_options.err_cb = function('s:GatherOutput', [l:line_list])
380     elseif l:output_stream is# 'both'
381         let l:job_options.out_cb = function('s:GatherOutput', [l:line_list])
382         let l:job_options.err_cb = function('s:GatherOutput', [l:line_list])
383     endif
384
385     let l:status = 'failed'
386
387     if get(g:, 'ale_run_synchronously') == 1
388         if get(g:, 'ale_emulate_job_failure') == 1
389             let l:job_id = 0
390         else
391             " Generate a fake job ID for tests.
392             let s:fake_job_id = get(s:, 'fake_job_id', 0) + 1
393             let l:job_id = s:fake_job_id
394         endif
395     elseif has('win32')
396         let l:job_id = ale#job#StartWithCmd(l:command, l:job_options)
397     else
398         let l:job_id = ale#job#Start(l:command, l:job_options)
399     endif
400
401     if l:job_id
402         let l:status = 'started'
403         let l:job_type = getbufvar(a:buffer, 'ale_job_type', 'all')
404
405         call ale#command#InitData(a:buffer)
406         let s:buffer_data[a:buffer].jobs[l:job_id] = l:job_type
407     endif
408
409     if g:ale_history_enabled
410         call ale#history#Add(a:buffer, l:status, l:job_id, l:command)
411     endif
412
413     if !l:job_id
414         return 0
415     endif
416
417     " We'll return this Dictionary. A `result_callback` can be assigned to it
418     " later for capturing the result of a:Callback.
419     "
420     " The `_deferred_job_id` is used for both checking the type of object, and
421     " for checking the job ID and status.
422     "
423     " The cwd is kept and used as the default value for the next command in
424     " the chain.
425     "
426     " The original command here is used in tests.
427     let l:result = {
428     \   '_deferred_job_id': l:job_id,
429     \   'executable': get(l:options, 'executable', ''),
430     \   'cwd': l:cwd,
431     \   'command': a:command,
432     \}
433
434     if get(g:, 'ale_run_synchronously') == 1 && l:job_id
435         if !exists('g:ale_run_synchronously_callbacks')
436             let g:ale_run_synchronously_callbacks = []
437         endif
438
439         if get(g:, 'ale_run_synchronously_emulate_commands', 0)
440             call add(
441             \   g:ale_run_synchronously_callbacks,
442             \   {exit_code, output -> [
443             \       extend(l:line_list, output),
444             \       l:job_options.exit_cb(l:job_id, exit_code),
445             \   ]}
446             \)
447         else
448             " Run a command synchronously if this test option is set.
449             call extend(l:line_list, systemlist(
450             \   type(l:command) is v:t_list
451             \       ? join(l:command[0:1]) . ' ' . ale#Escape(l:command[2])
452             \       : l:command
453             \))
454
455             " Don't capture output when the callbacks aren't set.
456             if !has_key(l:job_options, 'out_cb')
457             \&& !has_key(l:job_options, 'err_cb')
458                 let l:line_list = []
459             endif
460
461             call add(
462             \   g:ale_run_synchronously_callbacks,
463             \   {-> l:job_options.exit_cb(l:job_id, v:shell_error)}
464             \)
465         endif
466     endif
467
468     return l:result
469 endfunction
470
471 function! ale#command#IsDeferred(value) abort
472     return type(a:value) is v:t_dict && has_key(a:value, '_deferred_job_id')
473 endfunction