diff options
Diffstat (limited to 'runtime')
-rw-r--r-- | runtime/autoload/dist/ft.vim | 19 | ||||
-rw-r--r-- | runtime/autoload/man.vim | 529 | ||||
-rw-r--r-- | runtime/doc/api.txt | 1 | ||||
-rw-r--r-- | runtime/doc/filetype.txt | 4 | ||||
-rw-r--r-- | runtime/doc/map.txt | 10 | ||||
-rw-r--r-- | runtime/doc/vim_diff.txt | 2 | ||||
-rw-r--r-- | runtime/doc/windows.txt | 13 | ||||
-rw-r--r-- | runtime/filetype.vim | 6 | ||||
-rw-r--r-- | runtime/ftplugin/man.vim | 4 | ||||
-rw-r--r-- | runtime/lua/man.lua | 601 | ||||
-rw-r--r-- | runtime/lua/vim/filetype.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/filetype/detect.lua | 13 | ||||
-rw-r--r-- | runtime/plugin/man.lua | 34 | ||||
-rw-r--r-- | runtime/plugin/man.vim | 15 |
14 files changed, 688 insertions, 567 deletions
diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim index 77140d62b1..7333e5a7e7 100644 --- a/runtime/autoload/dist/ft.vim +++ b/runtime/autoload/dist/ft.vim @@ -468,7 +468,7 @@ endfunc " Returns true if file content looks like LambdaProlog module func IsLProlog() - " skip apparent comments and blank lines, what looks like + " skip apparent comments and blank lines, what looks like " LambdaProlog comment may be RAPID header let l = nextnonblank(1) while l > 0 && l < line('$') && getline(l) =~ '^\s*%' " LambdaProlog comment @@ -877,6 +877,23 @@ func dist#ft#FTsig() endif endfunc +" This function checks the first 100 lines of files matching "*.sil" to +" resolve detection between Swift Intermediate Language and SILE. +func dist#ft#FTsil() + for lnum in range(1, [line('$'), 100]->min()) + let line = getline(lnum) + if line =~ '^\s*[\\%]' + setf sile + return + elseif line =~ '^\s*\S' + setf sil + return + endif + endfor + " no clue, default to "sil" + setf sil +endfunc + func dist#ft#FTsys() if exists("g:filetype_sys") exe "setf " .. g:filetype_sys diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim deleted file mode 100644 index b8a73a64c9..0000000000 --- a/runtime/autoload/man.vim +++ /dev/null @@ -1,529 +0,0 @@ -" Maintainer: Anmol Sethi <hi@nhooyr.io> - -if exists('s:loaded_man') - finish -endif -let s:loaded_man = 1 - -let s:find_arg = '-w' -let s:localfile_arg = v:true " Always use -l if possible. #6683 - -function! man#init() abort - try - " Check for -l support. - call s:get_page(s:get_path('', 'man')) - catch /command error .*/ - let s:localfile_arg = v:false - endtry -endfunction - -function! man#open_page(count, mods, ...) abort - if a:0 > 2 - call s:error('too many arguments') - return - elseif a:0 == 0 - let ref = &filetype ==# 'man' ? expand('<cWORD>') : expand('<cword>') - if empty(ref) - call s:error('no identifier under cursor') - return - endif - elseif a:0 ==# 1 - let ref = a:1 - else - " Combine the name and sect into a manpage reference so that all - " verification/extraction can be kept in a single function. - " If a:2 is a reference as well, that is fine because it is the only - " reference that will match. - let ref = a:2.'('.a:1.')' - endif - try - let [sect, name] = s:extract_sect_and_name_ref(ref) - if a:count >= 0 - let sect = string(a:count) - endif - let path = s:verify_exists(sect, name) - let [sect, name] = s:extract_sect_and_name_path(path) - catch - call s:error(v:exception) - return - endtry - - let [l:buf, l:save_tfu] = [bufnr(), &tagfunc] - try - setlocal tagfunc=man#goto_tag - let l:target = l:name . '(' . l:sect . ')' - if a:mods !~# 'tab' && s:find_man() - execute 'silent keepalt tag' l:target - else - execute 'silent keepalt' a:mods 'stag' l:target - endif - call s:set_options(v:false) - finally - call setbufvar(l:buf, '&tagfunc', l:save_tfu) - endtry - - let b:man_sect = sect -endfunction - -" Called when a man:// buffer is opened. -function! man#read_page(ref) abort - try - let [sect, name] = s:extract_sect_and_name_ref(a:ref) - let path = s:verify_exists(sect, name) - let [sect, name] = s:extract_sect_and_name_path(path) - let page = s:get_page(path) - catch - call s:error(v:exception) - return - endtry - let b:man_sect = sect - call s:put_page(page) -endfunction - -" Handler for s:system() function. -function! s:system_handler(jobid, data, event) dict abort - if a:event is# 'stdout' || a:event is# 'stderr' - let self[a:event] .= join(a:data, "\n") - else - let self.exit_code = a:data - endif -endfunction - -" Run a system command and timeout after 30 seconds. -function! s:system(cmd, ...) abort - let opts = { - \ 'stdout': '', - \ 'stderr': '', - \ 'exit_code': 0, - \ 'on_stdout': function('s:system_handler'), - \ 'on_stderr': function('s:system_handler'), - \ 'on_exit': function('s:system_handler'), - \ } - let jobid = jobstart(a:cmd, opts) - - if jobid < 1 - throw printf('command error %d: %s', jobid, join(a:cmd)) - endif - - let res = jobwait([jobid], 30000) - if res[0] == -1 - try - call jobstop(jobid) - throw printf('command timed out: %s', join(a:cmd)) - catch /^Vim(call):E900:/ - endtry - elseif res[0] == -2 - throw printf('command interrupted: %s', join(a:cmd)) - endif - if opts.exit_code != 0 - throw printf("command error (%d) %s: %s", jobid, join(a:cmd), substitute(opts.stderr, '\_s\+$', '', &gdefault ? '' : 'g')) - endif - - return opts.stdout -endfunction - -function! s:set_options(pager) abort - setlocal noswapfile buftype=nofile bufhidden=hide - setlocal nomodified readonly nomodifiable - let b:pager = a:pager - setlocal filetype=man -endfunction - -function! s:get_page(path) abort - " Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065). - " Soft-wrap: ftplugin/man.vim sets wrap/breakindent/…. - " Hard-wrap: driven by `man`. - let manwidth = !get(g:, 'man_hardwrap', 1) ? 999 : (empty($MANWIDTH) ? winwidth(0) : $MANWIDTH) - " Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db). - " http://comments.gmane.org/gmane.editors.vim.devel/29085 - " Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces. - let cmd = ['env', 'MANPAGER=cat', 'MANWIDTH='.manwidth, 'MAN_KEEP_FORMATTING=1', 'man'] - return s:system(cmd + (s:localfile_arg ? ['-l', a:path] : [a:path])) -endfunction - -function! s:put_page(page) abort - setlocal modifiable noreadonly noswapfile - silent keepjumps %delete _ - silent put =a:page - while getline(1) =~# '^\s*$' - silent keepjumps 1delete _ - endwhile - " XXX: nroff justifies text by filling it with whitespace. That interacts - " badly with our use of $MANWIDTH=999. Hack around this by using a fixed - " size for those whitespace regions. - silent! keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g - 1 - lua require("man").highlight_man_page() - call s:set_options(v:false) -endfunction - -function! man#show_toc() abort - let bufname = bufname('%') - let info = getloclist(0, {'winid': 1}) - if !empty(info) && getwinvar(info.winid, 'qf_toc') ==# bufname - lopen - return - endif - - let toc = [] - let lnum = 2 - let last_line = line('$') - 1 - while lnum && lnum < last_line - let text = getline(lnum) - if text =~# '^\%( \{3\}\)\=\S.*$' - " if text is a section title - call add(toc, {'bufnr': bufnr('%'), 'lnum': lnum, 'text': text}) - elseif text =~# '^\s\+\%(+\|-\)\S\+' - " if text is a flag title. we strip whitespaces and prepend two - " spaces to have a consistent format in the loclist. - let text = ' ' .. substitute(text, '^\s*\(.\{-}\)\s*$', '\1', '') - call add(toc, {'bufnr': bufnr('%'), 'lnum': lnum, 'text': text}) - endif - let lnum = nextnonblank(lnum + 1) - endwhile - - call setloclist(0, toc, ' ') - call setloclist(0, [], 'a', {'title': 'Man TOC'}) - lopen - let w:qf_toc = bufname -endfunction - -" attempt to extract the name and sect out of 'name(sect)' -" otherwise just return the largest string of valid characters in ref -function! s:extract_sect_and_name_ref(ref) abort - if a:ref[0] ==# '-' " try ':Man -pandoc' with this disabled. - throw 'manpage name cannot start with ''-''' - endif - let ref = matchstr(a:ref, '[^()]\+([^()]\+)') - if empty(ref) - let name = matchstr(a:ref, '[^()]\+') - if empty(name) - throw 'manpage reference cannot contain only parentheses' - endif - return ['', s:spaces_to_underscores(name)] - endif - let left = split(ref, '(') - " see ':Man 3X curses' on why tolower. - " TODO(nhooyr) Not sure if this is portable across OSs - " but I have not seen a single uppercase section. - return [tolower(split(left[1], ')')[0]), s:spaces_to_underscores(left[0])] -endfunction - -" replace spaces in a man page name with underscores -" intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)'; -" while editing SQL source code, it's nice to visually select 'CREATE TABLE' -" and hit 'K', which requires this transformation -function! s:spaces_to_underscores(str) - return substitute(a:str, ' ', '_', 'g') -endfunction - -function! s:get_path(sect, name) abort - " Some man implementations (OpenBSD) return all available paths from the - " search command. Previously, this function would simply select the first one. - " - " However, some searches will report matches that are incorrect: - " man -w strlen may return string.3 followed by strlen.3, and therefore - " selecting the first would get us the wrong page. Thus, we must find the - " first matching one. - " - " There's yet another special case here. Consider the following: - " If you run man -w strlen and string.3 comes up first, this is a problem. We - " should search for a matching named one in the results list. - " However, if you search for man -w clock_gettime, you will *only* get - " clock_getres.2, which is the right page. Searching the resuls for - " clock_gettime will no longer work. In this case, we should just use the - " first one that was found in the correct section. - " - " Finally, we can avoid relying on -S or -s here since they are very - " inconsistently supported. Instead, call -w with a section and a name. - if empty(a:sect) - let results = split(s:system(['man', s:find_arg, a:name])) - else - let results = split(s:system(['man', s:find_arg, a:sect, a:name])) - endif - - if empty(results) - return '' - endif - - " find any that match the specified name - let namematches = filter(copy(results), 'fnamemodify(v:val, ":t") =~ a:name') - let sectmatches = [] - - if !empty(namematches) && !empty(a:sect) - let sectmatches = filter(copy(namematches), 'fnamemodify(v:val, ":e") == a:sect') - endif - - return substitute(get(sectmatches, 0, get(namematches, 0, results[0])), '\n\+$', '', '') -endfunction - -" s:verify_exists attempts to find the path to a manpage -" based on the passed section and name. -" -" 1. If the passed section is empty, b:man_default_sects is used. -" 2. If manpage could not be found with the given sect and name, -" then another attempt is made with b:man_default_sects. -" 3. If it still could not be found, then we try again without a section. -" 4. If still not found but $MANSECT is set, then we try again with $MANSECT -" unset. -" -" This function is careful to avoid duplicating a search if a previous -" step has already done it. i.e if we use b:man_default_sects in step 1, -" then we don't do it again in step 2. -function! s:verify_exists(sect, name) abort - let sect = a:sect - - if empty(sect) - " no section specified, so search with b:man_default_sects - if exists('b:man_default_sects') - let sects = split(b:man_default_sects, ',') - for sec in sects - try - let res = s:get_path(sec, a:name) - if !empty(res) - return res - endif - catch /^command error (/ - endtry - endfor - endif - else - " try with specified section - try - let res = s:get_path(sect, a:name) - if !empty(res) - return res - endif - catch /^command error (/ - endtry - - " try again with b:man_default_sects - if exists('b:man_default_sects') - let sects = split(b:man_default_sects, ',') - for sec in sects - try - let res = s:get_path(sec, a:name) - if !empty(res) - return res - endif - catch /^command error (/ - endtry - endfor - endif - endif - - " if none of the above worked, we will try with no section - try - let res = s:get_path('', a:name) - if !empty(res) - return res - endif - catch /^command error (/ - endtry - - " if that still didn't work, we will check for $MANSECT and try again with it - " unset - if !empty($MANSECT) - try - let MANSECT = $MANSECT - call setenv('MANSECT', v:null) - let res = s:get_path('', a:name) - if !empty(res) - return res - endif - catch /^command error (/ - finally - call setenv('MANSECT', MANSECT) - endtry - endif - - " finally, if that didn't work, there is no hope - throw 'no manual entry for ' . a:name -endfunction - -" Extracts the name/section from the 'path/name.sect', because sometimes the actual section is -" more specific than what we provided to `man` (try `:Man 3 App::CLI`). -" Also on linux, name seems to be case-insensitive. So for `:Man PRIntf`, we -" still want the name of the buffer to be 'printf'. -function! s:extract_sect_and_name_path(path) abort - let tail = fnamemodify(a:path, ':t') - if a:path =~# '\.\%([glx]z\|bz2\|lzma\|Z\)$' " valid extensions - let tail = fnamemodify(tail, ':r') - endif - let sect = matchstr(tail, '\.\zs[^.]\+$') - let name = matchstr(tail, '^.\+\ze\.') - return [sect, name] -endfunction - -function! s:find_man() abort - let l:win = 1 - while l:win <= winnr('$') - let l:buf = winbufnr(l:win) - if getbufvar(l:buf, '&filetype', '') ==# 'man' - execute l:win.'wincmd w' - return 1 - endif - let l:win += 1 - endwhile - return 0 -endfunction - -function! s:error(msg) abort - redraw - echohl ErrorMsg - echon 'man.vim: ' a:msg - echohl None -endfunction - -" see s:extract_sect_and_name_ref on why tolower(sect) -function! man#complete(arg_lead, cmd_line, cursor_pos) abort - let args = split(a:cmd_line) - let cmd_offset = index(args, 'Man') - if cmd_offset > 0 - " Prune all arguments up to :Man itself. Otherwise modifier commands like - " :tab, :vertical, etc. would lead to a wrong length. - let args = args[cmd_offset:] - endif - let l = len(args) - if l > 3 - return - elseif l ==# 1 - let name = '' - let sect = '' - elseif a:arg_lead =~# '^[^()]\+([^()]*$' - " cursor (|) is at ':Man printf(|' or ':Man 1 printf(|' - " The later is is allowed because of ':Man pri<TAB>'. - " It will offer 'priclass.d(1m)' even though section is specified as 1. - let tmp = split(a:arg_lead, '(') - let name = tmp[0] - let sect = tolower(get(tmp, 1, '')) - return s:complete(sect, '', name) - elseif args[1] !~# '^[^()]\+$' - " cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|' - " or ':Man 3() pri |' - return - elseif l ==# 2 - if empty(a:arg_lead) - " cursor (|) is at ':Man 1 |' - let name = '' - let sect = tolower(args[1]) - else - " cursor (|) is at ':Man pri|' - if a:arg_lead =~# '\/' - " if the name is a path, complete files - " TODO(nhooyr) why does this complete the last one automatically - return glob(a:arg_lead.'*', 0, 1) - endif - let name = a:arg_lead - let sect = '' - endif - elseif a:arg_lead !~# '^[^()]\+$' - " cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|' - return - else - " cursor (|) is at ':Man 3 pri|' - let name = a:arg_lead - let sect = tolower(args[1]) - endif - return s:complete(sect, sect, name) -endfunction - -function! s:get_paths(sect, name, do_fallback) abort - " callers must try-catch this, as some `man` implementations don't support `s:find_arg` - try - let mandirs = join(split(s:system(['man', s:find_arg]), ':\|\n'), ',') - let paths = globpath(mandirs, 'man?/'.a:name.'*.'.a:sect.'*', 0, 1) - try - " Prioritize the result from verify_exists as it obeys b:man_default_sects. - let first = s:verify_exists(a:sect, a:name) - let paths = filter(paths, 'v:val !=# first') - let paths = [first] + paths - catch - endtry - return paths - catch - if !a:do_fallback - throw v:exception - endif - - " Fallback to a single path, with the page we're trying to find. - try - return [s:verify_exists(a:sect, a:name)] - catch - return [] - endtry - endtry -endfunction - -function! s:complete(sect, psect, name) abort - let pages = s:get_paths(a:sect, a:name, v:false) - " We remove duplicates in case the same manpage in different languages was found. - return uniq(sort(map(pages, 's:format_candidate(v:val, a:psect)'), 'i')) -endfunction - -function! s:format_candidate(path, psect) abort - if a:path =~# '\.\%(pdf\|in\)$' " invalid extensions - return - endif - let [sect, name] = s:extract_sect_and_name_path(a:path) - if sect ==# a:psect - return name - elseif sect =~# a:psect.'.\+$' - " We include the section if the user provided section is a prefix - " of the actual section. - return name.'('.sect.')' - endif -endfunction - -" Called when Nvim is invoked as $MANPAGER. -function! man#init_pager() abort - if getline(1) =~# '^\s*$' - silent keepjumps 1delete _ - else - keepjumps 1 - endif - lua require("man").highlight_man_page() - " Guess the ref from the heading (which is usually uppercase, so we cannot - " know the correct casing, cf. `man glDrawArraysInstanced`). - let ref = substitute(matchstr(getline(1), '^[^)]\+)'), ' ', '_', 'g') - try - let b:man_sect = s:extract_sect_and_name_ref(ref)[0] - catch - let b:man_sect = '' - endtry - if -1 == match(bufname('%'), 'man:\/\/') " Avoid duplicate buffers, E95. - execute 'silent file man://'.tolower(fnameescape(ref)) - endif - - call s:set_options(v:true) -endfunction - -function! man#goto_tag(pattern, flags, info) abort - let [l:sect, l:name] = s:extract_sect_and_name_ref(a:pattern) - - let l:paths = s:get_paths(l:sect, l:name, v:true) - let l:structured = [] - - for l:path in l:paths - let [l:sect, l:name] = s:extract_sect_and_name_path(l:path) - let l:structured += [{ - \ 'name': l:name, - \ 'title': l:name . '(' . l:sect . ')' - \ }] - endfor - - if &cscopetag - " return only a single entry so we work well with :cstag (#11675) - let l:structured = l:structured[:0] - endif - - return map(l:structured, { - \ _, entry -> { - \ 'name': entry.name, - \ 'filename': 'man://' . entry.title, - \ 'cmd': '1' - \ } - \ }) -endfunction - -call man#init() diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 89baf84c86..82978d0b3c 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -1839,6 +1839,7 @@ nvim_parse_cmd({str}, {opts}) *nvim_parse_cmd()* • browse: (boolean) |:browse|. • confirm: (boolean) |:confirm|. • hide: (boolean) |:hide|. + • horizontal: (boolean) |:horizontal|. • keepalt: (boolean) |:keepalt|. • keepjumps: (boolean) |:keepjumps|. • keepmarks: (boolean) |:keepmarks|. diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt index 7fff74a963..9f8ef248f8 100644 --- a/runtime/doc/filetype.txt +++ b/runtime/doc/filetype.txt @@ -586,12 +586,12 @@ Local mappings: to the end of the file in Normal mode. This means "> " is inserted in each line. -MAN *ft-man-plugin* *:Man* *man.vim* +MAN *ft-man-plugin* *:Man* *man.lua* View manpages in Nvim. Supports highlighting, completion, locales, and navigation. Also see |find-manpage|. -man.vim will always attempt to reuse the closest man window (above/left) but +man.lua will always attempt to reuse the closest man window (above/left) but otherwise create a split. The case sensitivity of completion is controlled by 'fileignorecase'. diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index ca1ddaabd4..da6a305e89 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -1623,11 +1623,11 @@ The valid escape sequences are *<mods>* *<q-mods>* *:command-modifiers* <mods> The command modifiers, if specified. Otherwise, expands to nothing. Supported modifiers are |:aboveleft|, |:belowright|, - |:botright|, |:browse|, |:confirm|, |:hide|, |:keepalt|, - |:keepjumps|, |:keepmarks|, |:keeppatterns|, |:leftabove|, - |:lockmarks|, |:noautocmd|, |:noswapfile| |:rightbelow|, - |:sandbox|, |:silent|, |:tab|, |:topleft|, |:unsilent|, - |:verbose|, and |:vertical|. + |:botright|, |:browse|, |:confirm|, |:hide|, |:horizontal|, + |:keepalt|, |:keepjumps|, |:keepmarks|, |:keeppatterns|, + |:leftabove|, |:lockmarks|, |:noautocmd|, |:noswapfile| + |:rightbelow|, |:sandbox|, |:silent|, |:tab|, |:topleft|, + |:unsilent|, |:verbose|, and |:vertical|. Note that |:filter| is not supported. Examples: > command! -nargs=+ -complete=file MyEdit diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 0011cd9821..b013e00fe8 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -73,7 +73,7 @@ centralized reference of the differences. - 'wildmenu' is enabled - 'wildoptions' defaults to "pum,tagfile" -- |man.vim| plugin is enabled, so |:Man| is available by default. +- |man.lua| plugin is enabled, so |:Man| is available by default. - |matchit| plugin is enabled. To disable it in your config: > :let loaded_matchit = 1 diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt index 2627068a14..ddf4d09e92 100644 --- a/runtime/doc/windows.txt +++ b/runtime/doc/windows.txt @@ -235,9 +235,16 @@ and 'winminwidth' are relevant. *:vert* *:vertical* :vert[ical] {cmd} Execute {cmd}. If it contains a command that splits a window, - it will be split vertically. + it will be split vertically. For `vertical wincmd =` windows + will be equialized only vertically. Doesn't work for |:execute| and |:normal|. + *:hor* *:horizontal* +:hor[izontal] {cmd} + Execute {cmd}. Currently only makes a difference for + `horizontal wincmd =`, which will equal windows only + horizontally. + :lefta[bove] {cmd} *:lefta* *:leftabove* :abo[veleft] {cmd} *:abo* *:aboveleft* Execute {cmd}. If it contains a command that splits a window, @@ -530,6 +537,10 @@ CTRL-W = Make all windows (almost) equally high and wide, but use 'winheight' and 'winwidth' for the current window. Windows with 'winfixheight' set keep their height and windows with 'winfixwidth' set keep their width. + To equalize only vertically (make window equally high) use + `vertical wincmd =` + To equalize only horizontally (make window equally wide) use + `horizontal wincmd =` :res[ize] -N *:res* *:resize* *CTRL-W_-* CTRL-W - Decrease current window height by N (default 1). diff --git a/runtime/filetype.vim b/runtime/filetype.vim index 98d9df8b5c..d26cb808b5 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -1823,7 +1823,7 @@ au BufNewFile,BufRead *.score setf slrnsc au BufNewFile,BufRead *.st setf st " Smalltalk (and Rexx, TeX, and Visual Basic) -au BufNewFile,BufRead *.cls call dist#ft#FTcls() +au BufNewFile,BufRead *.cls call dist#ft#FTcls() " Smarty templates au BufNewFile,BufRead *.tpl setf smarty @@ -1930,8 +1930,8 @@ au BufNewFile,BufRead *.cm setf voscm au BufNewFile,BufRead *.swift setf swift au BufNewFile,BufRead *.swift.gyb setf swiftgyb -" Swift Intermediate Language -au BufNewFile,BufRead *.sil setf sil +" Swift Intermediate Language or SILE +au BufNewFile,BufRead *.sil call dist#ft#FTsil() " Sysctl au BufNewFile,BufRead */etc/sysctl.conf,*/etc/sysctl.d/*.conf setf sysctl diff --git a/runtime/ftplugin/man.vim b/runtime/ftplugin/man.vim index d7a08a9941..277ce3c0b3 100644 --- a/runtime/ftplugin/man.vim +++ b/runtime/ftplugin/man.vim @@ -17,12 +17,12 @@ setlocal iskeyword=@-@,:,a-z,A-Z,48-57,_,.,-,(,) setlocal nonumber norelativenumber setlocal foldcolumn=0 colorcolumn=0 nolist nofoldenable -setlocal tagfunc=man#goto_tag +setlocal tagfunc=v:lua.require'man'.goto_tag if !exists('g:no_plugin_maps') && !exists('g:no_man_maps') nnoremap <silent> <buffer> j gj nnoremap <silent> <buffer> k gk - nnoremap <silent> <buffer> gO :call man#show_toc()<CR> + nnoremap <silent> <buffer> gO :lua require'man'.show_toc()<CR> nnoremap <silent> <buffer> <2-LeftMouse> :Man<CR> if get(b:, 'pager') nnoremap <silent> <buffer> <nowait> q :lclose<CR><C-W>q diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 5da3d2a92f..4b8239ce74 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -1,7 +1,75 @@ require('vim.compat') +local api, fn = vim.api, vim.fn + +local find_arg = '-w' +local localfile_arg = true -- Always use -l if possible. #6683 local buf_hls = {} +local M = {} + +local function man_error(msg) + M.errormsg = 'man.lua: ' .. vim.inspect(msg) + error(M.errormsg) +end + +-- Run a system command and timeout after 30 seconds. +local function man_system(cmd, silent) + local stdout_data = {} + local stderr_data = {} + local stdout = vim.loop.new_pipe(false) + local stderr = vim.loop.new_pipe(false) + + local done = false + local exit_code + + local handle = vim.loop.spawn(cmd[1], { + args = vim.list_slice(cmd, 2), + stdio = { nil, stdout, stderr }, + }, function(code) + exit_code = code + stdout:close() + stderr:close() + done = true + end) + + if handle then + stdout:read_start(function(_, data) + stdout_data[#stdout_data + 1] = data + end) + stderr:read_start(function(_, data) + stderr_data[#stderr_data + 1] = data + end) + else + stdout:close() + stderr:close() + if not silent then + man_error(string.format('command error: %s', table.concat(cmd))) + end + end + + vim.wait(30000, function() + return done + end) + + if not done then + if handle then + vim.loop.shutdown(handle) + stdout:close() + stderr:close() + end + man_error(string.format('command timed out: %s', table.concat(cmd, ' '))) + end + + if exit_code ~= 0 and not silent then + man_error( + string.format("command error '%s': %s", table.concat(cmd, ' '), table.concat(stderr_data)) + ) + end + + return table.concat(stdout_data) +end + local function highlight_line(line, linenr) local chars = {} local prev_char = '' @@ -152,21 +220,540 @@ local function highlight_line(line, linenr) end local function highlight_man_page() - local mod = vim.api.nvim_buf_get_option(0, 'modifiable') - vim.api.nvim_buf_set_option(0, 'modifiable', true) + local mod = vim.bo.modifiable + vim.bo.modifiable = true - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local lines = api.nvim_buf_get_lines(0, 0, -1, false) for i, line in ipairs(lines) do lines[i] = highlight_line(line, i) end - vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + api.nvim_buf_set_lines(0, 0, -1, false, lines) for _, args in ipairs(buf_hls) do - vim.api.nvim_buf_add_highlight(unpack(args)) + api.nvim_buf_add_highlight(unpack(args)) end buf_hls = {} - vim.api.nvim_buf_set_option(0, 'modifiable', mod) + vim.bo.modifiable = mod +end + +-- replace spaces in a man page name with underscores +-- intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)'; +-- while editing SQL source code, it's nice to visually select 'CREATE TABLE' +-- and hit 'K', which requires this transformation +local function spaces_to_underscores(str) + local res = str:gsub('%s', '_') + return res +end + +local function get_path(sect, name, silent) + name = name or '' + sect = sect or '' + -- Some man implementations (OpenBSD) return all available paths from the + -- search command. Previously, this function would simply select the first one. + -- + -- However, some searches will report matches that are incorrect: + -- man -w strlen may return string.3 followed by strlen.3, and therefore + -- selecting the first would get us the wrong page. Thus, we must find the + -- first matching one. + -- + -- There's yet another special case here. Consider the following: + -- If you run man -w strlen and string.3 comes up first, this is a problem. We + -- should search for a matching named one in the results list. + -- However, if you search for man -w clock_gettime, you will *only* get + -- clock_getres.2, which is the right page. Searching the resuls for + -- clock_gettime will no longer work. In this case, we should just use the + -- first one that was found in the correct section. + -- + -- Finally, we can avoid relying on -S or -s here since they are very + -- inconsistently supported. Instead, call -w with a section and a name. + local cmd + if sect == '' then + cmd = { 'man', find_arg, name } + else + cmd = { 'man', find_arg, sect, name } + end + + local lines = man_system(cmd, silent) + if lines == nil then + return nil + end + + local results = vim.split(lines, '\n', { trimempty = true }) + + if #results == 0 then + return + end + + -- find any that match the specified name + local namematches = vim.tbl_filter(function(v) + return fn.fnamemodify(v, ':t'):match(name) + end, results) or {} + local sectmatches = {} + + if #namematches > 0 and sect ~= '' then + sectmatches = vim.tbl_filter(function(v) + return fn.fnamemodify(v, ':e') == sect + end, namematches) + end + + return fn.substitute(sectmatches[1] or namematches[1] or results[1], [[\n\+$]], '', '') +end + +local function matchstr(text, pat_or_re) + local re = type(pat_or_re) == 'string' and vim.regex(pat_or_re) or pat_or_re + + local s, e = re:match_str(text) + + if s == nil then + return + end + + return text:sub(vim.str_utfindex(text, s) + 1, vim.str_utfindex(text, e)) +end + +-- attempt to extract the name and sect out of 'name(sect)' +-- otherwise just return the largest string of valid characters in ref +local function extract_sect_and_name_ref(ref) + ref = ref or '' + if ref:sub(1, 1) == '-' then -- try ':Man -pandoc' with this disabled. + man_error("manpage name cannot start with '-'") + end + local ref1 = ref:match('[^()]+%([^()]+%)') + if not ref1 then + local name = ref:match('[^()]+') + if not name then + man_error('manpage reference cannot contain only parentheses: ' .. ref) + end + return '', spaces_to_underscores(name) + end + local parts = vim.split(ref1, '(', { plain = true }) + -- see ':Man 3X curses' on why tolower. + -- TODO(nhooyr) Not sure if this is portable across OSs + -- but I have not seen a single uppercase section. + local sect = vim.split(parts[2] or '', ')', { plain = true })[1]:lower() + local name = spaces_to_underscores(parts[1]) + return sect, name +end + +-- verify_exists attempts to find the path to a manpage +-- based on the passed section and name. +-- +-- 1. If manpage could not be found with the given sect and name, +-- then try all the sections in b:man_default_sects. +-- 2. If it still could not be found, then we try again without a section. +-- 3. If still not found but $MANSECT is set, then we try again with $MANSECT +-- unset. +local function verify_exists(sect, name) + if sect and sect ~= '' then + local ret = get_path(sect, name, true) + if ret then + return ret + end + end + + if vim.b.man_default_sects ~= nil then + local sects = vim.split(vim.b.man_default_sects, ',', { plain = true, trimempty = true }) + for _, sec in ipairs(sects) do + local ret = get_path(sec, name, true) + if ret then + return ret + end + end + end + + -- if none of the above worked, we will try with no section + local res_empty_sect = get_path('', name, true) + if res_empty_sect then + return res_empty_sect + end + + -- if that still didn't work, we will check for $MANSECT and try again with it + -- unset + if vim.env.MANSECT then + local mansect = vim.env.MANSECT + vim.env.MANSECT = nil + local res = get_path('', name, true) + vim.env.MANSECT = mansect + if res then + return res + end + end + + -- finally, if that didn't work, there is no hope + man_error('no manual entry for ' .. name) +end + +local EXT_RE = vim.regex([[\.\%([glx]z\|bz2\|lzma\|Z\)$]]) + +-- Extracts the name/section from the 'path/name.sect', because sometimes the actual section is +-- more specific than what we provided to `man` (try `:Man 3 App::CLI`). +-- Also on linux, name seems to be case-insensitive. So for `:Man PRIntf`, we +-- still want the name of the buffer to be 'printf'. +local function extract_sect_and_name_path(path) + local tail = fn.fnamemodify(path, ':t') + if EXT_RE:match_str(path) then -- valid extensions + tail = fn.fnamemodify(tail, ':r') + end + local name, sect = tail:match('^(.+)%.([^.]+)$') + return sect, name +end + +local function find_man() + local win = 1 + while win <= fn.winnr('$') do + local buf = fn.winbufnr(win) + if vim.bo[buf].filetype == 'man' then + vim.cmd(win .. 'wincmd w') + return true + end + win = win + 1 + end + return false +end + +local function set_options(pager) + vim.bo.swapfile = false + vim.bo.buftype = 'nofile' + vim.bo.bufhidden = 'hide' + vim.bo.modified = false + vim.bo.readonly = true + vim.bo.modifiable = false + vim.b.pager = pager + vim.bo.filetype = 'man' +end + +local function get_page(path, silent) + -- Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065). + -- Soft-wrap: ftplugin/man.lua sets wrap/breakindent/…. + -- Hard-wrap: driven by `man`. + local manwidth + if (vim.g.man_hardwrap or 1) ~= 1 then + manwidth = 999 + elseif vim.env.MANWIDTH then + manwidth = vim.env.MANWIDTH + else + manwidth = api.nvim_win_get_width(0) + end + -- Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db). + -- http://comments.gmane.org/gmane.editors.vim.devel/29085 + -- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces. + local cmd = { 'env', 'MANPAGER=cat', 'MANWIDTH=' .. manwidth, 'MAN_KEEP_FORMATTING=1', 'man' } + if localfile_arg then + cmd[#cmd + 1] = '-l' + end + cmd[#cmd + 1] = path + return man_system(cmd, silent) +end + +local function put_page(page) + vim.bo.modified = true + vim.bo.readonly = false + vim.bo.swapfile = false + + api.nvim_buf_set_lines(0, 0, -1, false, vim.split(page, '\n')) + + while fn.getline(1):match('^%s*$') do + api.nvim_buf_set_lines(0, 0, 1, false, {}) + end + -- XXX: nroff justifies text by filling it with whitespace. That interacts + -- badly with our use of $MANWIDTH=999. Hack around this by using a fixed + -- size for those whitespace regions. + vim.cmd([[silent! keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g]]) + vim.cmd('1') -- Move cursor to first line + highlight_man_page() + set_options(false) +end + +local function format_candidate(path, psect) + if matchstr(path, [[\.\%(pdf\|in\)$]]) then -- invalid extensions + return '' + end + local sect, name = extract_sect_and_name_path(path) + if sect == psect then + return name + elseif sect and name and matchstr(sect, psect .. '.\\+$') then -- invalid extensions + -- We include the section if the user provided section is a prefix + -- of the actual section. + return ('%s(%s)'):format(name, sect) + end + return '' end -return { highlight_man_page = highlight_man_page } +local function get_paths(sect, name, do_fallback) + -- callers must try-catch this, as some `man` implementations don't support `s:find_arg` + local ok, ret = pcall(function() + local mandirs = + table.concat(vim.split(man_system({ 'man', find_arg }), '[:\n]', { trimempty = true }), ',') + local paths = fn.globpath(mandirs, 'man?/' .. name .. '*.' .. sect .. '*', false, true) + pcall(function() + -- Prioritize the result from verify_exists as it obeys b:man_default_sects. + local first = verify_exists(sect, name) + paths = vim.tbl_filter(function(v) + return v ~= first + end, paths) + paths = { first, unpack(paths) } + end) + return paths + end) + + if not ok then + if not do_fallback then + error(ret) + end + + -- Fallback to a single path, with the page we're trying to find. + ok, ret = pcall(verify_exists, sect, name) + + return { ok and ret or nil } + end + return ret or {} +end + +local function complete(sect, psect, name) + local pages = get_paths(sect, name, false) + -- We remove duplicates in case the same manpage in different languages was found. + return fn.uniq(fn.sort(vim.tbl_map(function(v) + return format_candidate(v, psect) + end, pages) or {}, 'i')) +end + +-- see extract_sect_and_name_ref on why tolower(sect) +function M.man_complete(arg_lead, cmd_line, _) + local args = vim.split(cmd_line, '%s+', { trimempty = true }) + local cmd_offset = fn.index(args, 'Man') + if cmd_offset > 0 then + -- Prune all arguments up to :Man itself. Otherwise modifier commands like + -- :tab, :vertical, etc. would lead to a wrong length. + args = vim.list_slice(args, cmd_offset + 1) + end + + if #args > 3 then + return {} + end + + if #args == 1 then + -- returning full completion is laggy. Require some arg_lead to complete + -- return complete('', '', '') + return {} + end + + if arg_lead:match('^[^()]+%([^()]*$') then + -- cursor (|) is at ':Man printf(|' or ':Man 1 printf(|' + -- The later is is allowed because of ':Man pri<TAB>'. + -- It will offer 'priclass.d(1m)' even though section is specified as 1. + local tmp = vim.split(arg_lead, '(', { plain = true }) + local name = tmp[1] + local sect = (tmp[2] or ''):lower() + return complete(sect, '', name) + end + + if not args[2]:match('^[^()]+$') then + -- cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|' + -- or ':Man 3() pri |' + return {} + end + + if #args == 2 then + local name, sect + if arg_lead == '' then + -- cursor (|) is at ':Man 1 |' + name = '' + sect = args[1]:lower() + else + -- cursor (|) is at ':Man pri|' + if arg_lead:match('/') then + -- if the name is a path, complete files + -- TODO(nhooyr) why does this complete the last one automatically + return fn.glob(arg_lead .. '*', false, true) + end + name = arg_lead + sect = '' + end + return complete(sect, sect, name) + end + + if not arg_lead:match('[^()]+$') then + -- cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|' + return {} + end + + -- cursor (|) is at ':Man 3 pri|' + local name = arg_lead + local sect = args[2]:lower() + return complete(sect, sect, name) +end + +function M.goto_tag(pattern, _, _) + local sect, name = extract_sect_and_name_ref(pattern) + + local paths = get_paths(sect, name, true) + local structured = {} + + for _, path in ipairs(paths) do + sect, name = extract_sect_and_name_path(path) + if sect and name then + structured[#structured + 1] = { + name = name, + title = name .. '(' .. sect .. ')', + } + end + end + + if vim.o.cscopetag then + -- return only a single entry so we work well with :cstag (#11675) + structured = { structured[1] } + end + + return vim.tbl_map(function(entry) + return { + name = entry.name, + filename = 'man://' .. entry.title, + cmd = '1', + } + end, structured) +end + +-- Called when Nvim is invoked as $MANPAGER. +function M.init_pager() + if fn.getline(1):match('^%s*$') then + api.nvim_buf_set_lines(0, 0, 1, false, {}) + else + vim.cmd('keepjumps 1') + end + highlight_man_page() + -- Guess the ref from the heading (which is usually uppercase, so we cannot + -- know the correct casing, cf. `man glDrawArraysInstanced`). + local ref = fn.substitute(matchstr(fn.getline(1), [[^[^)]\+)]]) or '', ' ', '_', 'g') + local ok, res = pcall(extract_sect_and_name_ref, ref) + vim.b.man_sect = ok and res or '' + + if not fn.bufname('%'):match('man://') then -- Avoid duplicate buffers, E95. + vim.cmd.file({ 'man://' .. fn.fnameescape(ref):lower(), mods = { silent = true } }) + end + + set_options(true) +end + +function M.open_page(count, smods, args) + if #args > 2 then + man_error('too many arguments') + end + + local ref + if #args == 0 then + ref = vim.bo.filetype == 'man' and fn.expand('<cWORD>') or fn.expand('<cword>') + if ref == '' then + man_error('no identifier under cursor') + end + elseif #args == 1 then + ref = args[1] + else + -- Combine the name and sect into a manpage reference so that all + -- verification/extraction can be kept in a single function. + -- If args[2] is a reference as well, that is fine because it is the only + -- reference that will match. + ref = ('%s(%s)'):format(args[2], args[1]) + end + + local sect, name = extract_sect_and_name_ref(ref) + if count >= 0 then + sect = tostring(count) + end + + local path = verify_exists(sect, name) + sect, name = extract_sect_and_name_path(path) + + local buf = fn.bufnr() + local save_tfu = vim.bo[buf].tagfunc + vim.bo[buf].tagfunc = "v:lua.require'man'.goto_tag" + + local target = ('%s(%s)'):format(name, sect) + + local ok, ret = pcall(function() + if not smods.tab and find_man() then + vim.cmd.tag({ target, mods = { silent = true, keepalt = true } }) + else + smods.silent = true + smods.keepalt = true + vim.cmd.stag({ target, mods = smods }) + end + end) + + vim.bo[buf].tagfunc = save_tfu + + if not ok then + error(ret) + else + set_options(false) + end + + vim.b.man_sect = sect +end + +-- Called when a man:// buffer is opened. +function M.read_page(ref) + local sect, name = extract_sect_and_name_ref(ref) + local path = verify_exists(sect, name) + sect = extract_sect_and_name_path(path) + local page = get_page(path) + vim.b.man_sect = sect + put_page(page) +end + +function M.show_toc() + local bufname = fn.bufname('%') + local info = fn.getloclist(0, { winid = 1 }) + if info ~= '' and vim.w[info.winid].qf_toc == bufname then + vim.cmd.lopen() + return + end + + local toc = {} + local lnum = 2 + local last_line = fn.line('$') - 1 + local section_title_re = vim.regex([[^\%( \{3\}\)\=\S.*$]]) + local flag_title_re = vim.regex([[^\s\+\%(+\|-\)\S\+]]) + while lnum and lnum < last_line do + local text = fn.getline(lnum) + if section_title_re:match_str(text) then + -- if text is a section title + toc[#toc + 1] = { + bufnr = fn.bufnr('%'), + lnum = lnum, + text = text, + } + elseif flag_title_re:match_str(text) then + -- if text is a flag title. we strip whitespaces and prepend two + -- spaces to have a consistent format in the loclist. + toc[#toc + 1] = { + bufnr = fn.bufnr('%'), + lnum = lnum, + text = ' ' .. fn.substitute(text, [[^\s*\(.\{-}\)\s*$]], [[\1]], ''), + } + end + lnum = fn.nextnonblank(lnum + 1) + end + + fn.setloclist(0, toc, ' ') + fn.setloclist(0, {}, 'a', { title = 'Man TOC' }) + vim.cmd.lopen() + vim.w.qf_toc = bufname +end + +local function init() + local path = get_path('', 'man', true) + local page + if path ~= nil then + -- Check for -l support. + page = get_page(path, true) + end + + if page == '' or page == nil then + localfile_arg = false + end +end + +init() + +return M diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index fcd697a7c1..6306605641 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -902,7 +902,9 @@ local extension = { sig = function(path, bufnr) return require('vim.filetype.detect').sig(bufnr) end, - sil = 'sil', + sil = function(path, bufnr) + return require('vim.filetype.detect').sil(bufnr) + end, sim = 'simula', ['s85'] = 'sinda', sin = 'sinda', diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index 2be9dcff88..7fc7f1b7ca 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -1194,6 +1194,19 @@ function M.shell(path, contents, name) return name end +-- Swift Intermediate Language or SILE +function M.sil(bufnr) + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if line:find('^%s*[\\%%]') then + return 'sile' + elseif line:find('^%s*%S') then + return 'sil' + end + end + -- No clue, default to "sil" + return 'sil' +end + -- SMIL or SNMP MIB file function M.smi(bufnr) local line = getlines(bufnr, 1) diff --git a/runtime/plugin/man.lua b/runtime/plugin/man.lua new file mode 100644 index 0000000000..4b1528b0cb --- /dev/null +++ b/runtime/plugin/man.lua @@ -0,0 +1,34 @@ +if vim.g.loaded_man ~= nil then + return +end +vim.g.loaded_man = true + +vim.api.nvim_create_user_command('Man', function(params) + local man = require('man') + if params.bang then + man.init_pager() + else + local ok, err = pcall(man.open_page, params.count, params.smods, params.fargs) + if not ok then + vim.notify(man.errormsg or err, vim.log.levels.ERROR) + end + end +end, { + bang = true, + bar = true, + addr = 'other', + nargs = '*', + complete = function(...) + return require('man').man_complete(...) + end, +}) + +local augroup = vim.api.nvim_create_augroup('man', {}) + +vim.api.nvim_create_autocmd('BufReadCmd', { + group = augroup, + pattern = 'man://*', + callback = function(params) + require('man').read_page(vim.fn.matchstr(params.match, 'man://\\zs.*')) + end, +}) diff --git a/runtime/plugin/man.vim b/runtime/plugin/man.vim deleted file mode 100644 index b10677593f..0000000000 --- a/runtime/plugin/man.vim +++ /dev/null @@ -1,15 +0,0 @@ -" Maintainer: Anmol Sethi <hi@nhooyr.io> - -if exists('g:loaded_man') - finish -endif -let g:loaded_man = 1 - -command! -bang -bar -addr=other -complete=customlist,man#complete -nargs=* Man - \ if <bang>0 | call man#init_pager() | - \ else | call man#open_page(<count>, <q-mods>, <f-args>) | endif - -augroup man - autocmd! - autocmd BufReadCmd man://* call man#read_page(matchstr(expand('<amatch>'), 'man://\zs.*')) -augroup END |