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: Functions for working with paths in the filesystem.
4 " simplify a path, and fix annoying issues with paths on Windows.
6 " Forward slashes are changed to back slashes so path equality works better
7 " on Windows. Back slashes are changed to forward slashes on Unix.
9 " Unix paths can technically contain back slashes, but in practice no path
10 " should, and replacing back slashes with forward slashes makes linters work
11 " in environments like MSYS.
13 " Paths starting with more than one forward slash are changed to only one
14 " forward slash, to prevent the paths being treated as special MSYS paths.
15 function! ale#path#Simplify(path) abort
17 let l:unix_path = substitute(a:path, '\\', '/', 'g')
19 return substitute(simplify(l:unix_path), '^//\+', '/', 'g') " no-custom-checks
22 let l:win_path = substitute(a:path, '/', '\\', 'g')
24 return substitute(simplify(l:win_path), '^\\\+', '\', 'g') " no-custom-checks
27 " Simplify a path without a Windows drive letter.
28 " This function can be used for checking if paths are equal.
29 function! ale#path#RemoveDriveLetter(path) abort
30 return has('win32') && a:path[1:2] is# ':\'
31 \ ? ale#path#Simplify(a:path[2:])
32 \ : ale#path#Simplify(a:path)
35 " Given a buffer and a filename, find the nearest file by searching upwards
36 " through the paths relative to the given buffer.
37 function! ale#path#FindNearestFile(buffer, filename) abort
38 let l:buffer_filename = fnamemodify(bufname(a:buffer), ':p')
39 let l:buffer_filename = fnameescape(l:buffer_filename)
41 let l:relative_path = findfile(a:filename, l:buffer_filename . ';')
43 if !empty(l:relative_path)
44 return fnamemodify(l:relative_path, ':p')
50 " Given a buffer and a directory name, find the nearest directory by searching upwards
51 " through the paths relative to the given buffer.
52 function! ale#path#FindNearestDirectory(buffer, directory_name) abort
53 let l:buffer_filename = fnamemodify(bufname(a:buffer), ':p')
54 let l:buffer_filename = fnameescape(l:buffer_filename)
56 let l:relative_path = finddir(a:directory_name, l:buffer_filename . ';')
58 if !empty(l:relative_path)
59 return fnamemodify(l:relative_path, ':p')
65 " Given a buffer, a string to search for, and a global fallback for when
66 " the search fails, look for a file in parent paths, and if that fails,
67 " use the global fallback path instead.
68 function! ale#path#ResolveLocalPath(buffer, search_string, global_fallback) abort
69 " Search for a locally installed file first.
70 let l:path = ale#path#FindNearestFile(a:buffer, a:search_string)
72 " If the search fails, try the global executable instead.
74 let l:path = a:global_fallback
80 " Given a buffer number, a base variable name, and a list of paths to search
81 " for in ancestor directories, detect the executable path for a program.
82 function! ale#path#FindNearestExecutable(buffer, path_list) abort
83 for l:path in a:path_list
84 if ale#path#IsAbsolute(l:path)
85 let l:executable = filereadable(l:path) ? l:path : ''
87 let l:executable = ale#path#FindNearestFile(a:buffer, l:path)
90 if !empty(l:executable)
98 " Given a buffer number, a base variable name, and a list of paths to search
99 " for in ancestor directories, detect the executable path for a program.
101 " The use_global and executable options for the relevant program will be used.
102 function! ale#path#FindExecutable(buffer, base_var_name, path_list) abort
103 if ale#Var(a:buffer, a:base_var_name . '_use_global')
104 return ale#Var(a:buffer, a:base_var_name . '_executable')
107 let l:nearest = ale#path#FindNearestExecutable(a:buffer, a:path_list)
113 return ale#Var(a:buffer, a:base_var_name . '_executable')
116 " Return 1 if a path is an absolute path.
117 function! ale#path#IsAbsolute(filename) abort
118 if has('win32') && a:filename[:0] is# '\'
122 " Check for /foo and C:\foo, etc.
123 return a:filename[:0] is# '/' || a:filename[1:2] is# ':\'
126 let s:temp_dir = ale#path#Simplify(fnamemodify(ale#util#Tempname(), ':h:h'))
127 let s:resolved_temp_dir = resolve(s:temp_dir)
129 " Given a filename, return 1 if the file represents some temporary file
130 " created by Vim. If the temporary location is symlinked (e.g. macOS), some
131 " linters may report the resolved version of the path, so both are checked.
132 function! ale#path#IsTempName(filename) abort
133 let l:filename = ale#path#Simplify(a:filename)
135 return l:filename[:len(s:temp_dir) - 1] is# s:temp_dir
136 \|| l:filename[:len(s:resolved_temp_dir) - 1] is# s:resolved_temp_dir
139 " Given a base directory, which must not have a trailing slash, and a
140 " filename, which may have an absolute path a path relative to the base
141 " directory, return the absolute path to the file.
142 function! ale#path#GetAbsPath(base_directory, filename) abort
143 if ale#path#IsAbsolute(a:filename)
144 return ale#path#Simplify(a:filename)
147 let l:sep = has('win32') ? '\' : '/'
149 return ale#path#Simplify(a:base_directory . l:sep . a:filename)
152 " Given a path, return the directory name for that path, with no trailing
153 " slashes. If the argument is empty(), return an empty string.
154 function! ale#path#Dirname(path) abort
159 " For /foo/bar/ we need :h:h to get /foo
160 if a:path[-1:] is# '/' || (has('win32') && a:path[-1:] is# '\')
161 return fnamemodify(a:path, ':h:h')
164 return fnamemodify(a:path, ':h')
167 " Given a buffer number and a relative or absolute path, return 1 if the
168 " two paths represent the same file on disk.
169 function! ale#path#IsBufferPath(buffer, complex_filename) abort
170 " If the path is one of many different names for stdin, we have a match.
171 if a:complex_filename is# '-'
172 \|| a:complex_filename is# 'stdin'
173 \|| a:complex_filename[:0] is# '<'
177 let l:test_filename = ale#path#Simplify(a:complex_filename)
179 if l:test_filename[:1] is# './'
180 let l:test_filename = l:test_filename[2:]
183 if l:test_filename[:1] is# '..'
184 " Remove ../../ etc. from the front of the path.
185 let l:test_filename = substitute(l:test_filename, '\v^(\.\.[/\\])+', '/', '')
188 " Use the basename for temporary files, as they are likely our files.
189 if ale#path#IsTempName(l:test_filename)
190 let l:test_filename = fnamemodify(l:test_filename, ':t')
193 let l:buffer_filename = expand('#' . a:buffer . ':p')
195 return l:buffer_filename is# l:test_filename
196 \ || l:buffer_filename[-len(l:test_filename):] is# l:test_filename
199 " Given a path, return every component of the path, moving upwards.
200 function! ale#path#Upwards(path) abort
201 let l:pattern = has('win32') ? '\v/+|\\+' : '\v/+'
202 let l:sep = has('win32') ? '\' : '/'
203 let l:parts = split(ale#path#Simplify(a:path), l:pattern)
206 while !empty(l:parts)
207 call add(l:path_list, join(l:parts, l:sep))
208 let l:parts = l:parts[:-2]
211 if has('win32') && a:path =~# '^[a-zA-z]:\'
212 " Add \ to C: for C:\, etc.
213 let l:path_list[-1] .= '\'
214 elseif a:path[0] is# '/'
215 " If the path starts with /, even on Windows, add / and / to all paths.
216 call map(l:path_list, '''/'' . v:val')
217 call add(l:path_list, '/')
223 " Convert a filesystem path to a file:// URI
224 " relatives paths will not be prefixed with the protocol.
225 " For Windows paths, the `:` in C:\ etc. will not be percent-encoded.
226 function! ale#path#ToFileURI(path) abort
227 let l:has_drive_letter = a:path[1:2] is# ':\'
230 \ ((l:has_drive_letter || a:path[:0] is# '/') ? 'file://' : '')
231 \ . (l:has_drive_letter ? '/' . a:path[:2] : '')
232 \ . ale#uri#Encode(l:has_drive_letter ? a:path[3:] : a:path),
239 function! ale#path#FromFileURI(uri) abort
240 if a:uri[:6] is? 'file://'
241 let l:encoded_path = a:uri[7:]
242 elseif a:uri[:4] is? 'file:'
243 let l:encoded_path = a:uri[5:]
245 let l:encoded_path = a:uri
248 let l:path = ale#uri#Decode(l:encoded_path)
250 " If the path is like /C:/foo/bar, it should be C:\foo\bar instead.
251 if has('win32') && l:path =~# '^/[a-zA-Z][:|]'
252 let l:path = substitute(l:path[1:], '/', '\\', 'g')
253 let l:path = l:path[0] . ':' . l:path[2:]