1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-03 23:50:05 +08:00
SpaceVim/bundle/open-browser.vim/autoload/vital/__openbrowser__/OpenBrowser.vim

777 lines
22 KiB
VimL
Raw Normal View History

2020-06-13 14:06:35 +08:00
" vim:foldmethod=marker:fen:
scriptencoding utf-8
let s:save_cpo = &cpo
set cpo&vim
function! s:_vital_depends() abort
return [
\ 'Web.URI',
\ 'Vim.Message',
\ 'Data.Optional',
\ 'Data.String',
\ 'Web.HTTP',
\ 'Vim.Buffer',
\
\ 'OpenBrowser.Opener',
\]
endfunction
function! s:_vital_loaded(V) abort
let s:URI = a:V.import('Web.URI')
let s:Msg = a:V.import('Vim.Message')
let s:O = a:V.import('Data.Optional')
let s:_truncate_skipping = a:V.import('Data.String').truncate_skipping
let s:_encodeURIComponent = a:V.import('Web.HTTP').encodeURIComponent
let s:_get_last_selected = a:V.import('Vim.Buffer').get_last_selected
let s:is_cygwin = has('win32unix')
let s:is_mswin = has('win16') || has('win32') || has('win64')
let s:use_wslpath = has('unix') && filereadable('/proc/version_signature')
\ && get(readfile('/proc/version_signature', 'b', 1), 0, '') =~# '^Microsoft'
\ && executable('wslpath')
let s:Opener = a:V.import('OpenBrowser.Opener')
let s:URIExtractor = a:V.import('OpenBrowser.URIExtractor')
let s:vimproc_is_installed = globpath(&rtp, 'autoload/vimproc.vim') isnot# ''
endfunction
let s:NONE = []
lockvar s:NONE
function! s:new(config) abort
return {
\ 'config': a:config,
\
\ 'open': function('s:_OpenBrowser_open'),
\ 'search': function('s:_OpenBrowser_search'),
\ 'smart_search': function('s:_OpenBrowser_smart_search'),
\ 'cmd_open': function('s:_OpenBrowser_cmd_open'),
\ 'cmd_search': function('s:_OpenBrowser_cmd_search'),
\ 'cmd_smart_search': function('s:_OpenBrowser_cmd_smart_search'),
\ 'cmd_search_complete': function('s:_OpenBrowser_cmd_search_complete'),
\ 'keymap_open': function('s:_OpenBrowser_keymap_open'),
\ 'keymap_search': function('s:_OpenBrowser_keymap_search'),
\ 'keymap_smart_search': function('s:_OpenBrowser_keymap_smart_search'),
\}
endfunction
" @param uri URI object or String
function! s:_OpenBrowser_open(uri, ...) abort dict
let uri = a:uri
let options = a:0 && type(a:1) ==# type([]) ? a:1 : []
if type(uri) isnot# type('')
call s:_throw('s:OpenBrowser.open() received non-String argument: uri = ' . string(uri))
endif
" TODO: After deprecating Vim 7.x, refactoring by:
" * Using s:O.map(), ...
" https://github.com/vim-jp/vital.vim/issues/576
" * Using partial, lambda, ...
let builder = s:_get_opener_builder(a:uri, self.config)
let failed = 0
if s:O.empty(builder)
let failed = 1
else
" Open URI in a browser / Open a file in Vim
let b = s:O.get(builder)
" Show message
if b.type is# 'shellcmd'
redraw!
let format_message = self.config.get('format_message')
if self.config.get('message_verbosity') >= 2 && format_message.msg isnot# ''
let msg = s:_expand_format_message(format_message,
\ {
\ 'uri' : uri,
\ 'done' : 0,
\ 'command' : '',
\ })
echo msg
endif
endif
if type(b.cmd.args) == v:t_string
"String (Windows)
let b.cmd.args = join([b.cmd.args] + map(copy(options), 'shellescape(v:val)'), ' ')
else
"Array (Other platforms)
let b.cmd.args += options
endif
let opener = b.build()
let failed = !opener.open()
if !failed && b.type is# 'shellcmd'
" Show message
if self.config.get('message_verbosity') >= 2 && format_message.msg isnot# ''
redraw
let msg = s:_expand_format_message(format_message,
\ {
\ 'uri' : uri,
\ 'done' : 1,
\ 'command' : b.cmd.name,
\ })
echo msg
endif
" XXX: Vim looses a focus after opening URI...
" Is this same as non-Windows platform?
if g:openbrowser_force_foreground_after_open && s:is_mswin
augroup openbrowser-focuslost
autocmd!
autocmd FocusLost * call foreground() | autocmd! openbrowser FocusLost
augroup END
endif
endif
endif
if failed
if self.config.get('message_verbosity') >= 1
call s:Msg.warn("open-browser doesn't know how to open '" . uri . "'.")
endif
endif
endfunction
" Returns s:O.some(builder) or s:O.none().
" Builder is either Ex command opener or shell command opener.
" Ex command opener builds an opener which opens a given file in Vim.
" Shell command builder builds an opener which opens a given URI in a browser.
function! s:_get_opener_builder(uristr, config) abort
let [uristr, config] = [a:uristr, a:config]
let uriobj = s:URI.new_from_uri_like_string(uristr, s:NONE)
if s:_seems_path(uristr) " Existed file path or 'file://'
" Convert to full path.
if stridx(uristr, 'file://') is# 0 " file://
let fullpath = substitute(uristr, '^file://', '', '')
elseif uristr[0] is# '/' " full path
let fullpath = uristr
else " relative path
let fullpath = s:_convert_to_fullpath(uristr)
endif
if config.get('open_filepath_in_vim')
let fullpath = tr(fullpath, '\', '/')
let command = config.get('open_vim_command')
let builder = s:_new_excmd_opener_builder(join([command, fullpath]))
return s:O.some(builder)
else
let fullpath = tr(fullpath, '\', '/')
" Windows Subsystem for Linux: Convert Unix path to Windows UNC path
if s:use_wslpath
let fullpath = substitute(
\ system('wslpath -am ' . shellescape(fullpath)), '\n', '', '')
endif
" Convert to file:// string.
" NOTE: cygwin cannot treat file:// URI,
" pass a string as fullpath.
if !s:is_cygwin
let fullpath = 'file://' . fullpath
endif
return s:_get_shellcmd_opener_builder(fullpath, config)
endif
elseif s:_valid_uri(uriobj) " other URI
" Fix scheme, host, path.
" e.g.: "ttp" => "http"
for where in ['scheme', 'host', 'path']
let fix = config.get('fix_'.where.'s')
let value = uriobj[where]()
if has_key(fix, value)
call call(uriobj[where], [fix[value]], uriobj)
endif
endfor
let uristr = uriobj.to_string()
return s:_get_shellcmd_opener_builder(uristr, config)
endif
return s:O.none()
endfunction
" Returns builder which builds Ex command opener.
" `builder.build().open()` will open a file in Vim.
function! s:_new_excmd_opener_builder(excmd) abort
let builder = {
\ 'type': 'excmd',
\ 'excmd': a:excmd,
\}
function! builder.build() abort
return s:Opener.new_from_excmd(self.excmd)
endfunction
return builder
endfunction
" Returns builder which builds shell command opener.
" `builder.build().open()` will open the URI in a browser.
function! s:_new_shellcmd_opener_builder(cmd, execmd, uri, use_vimproc) abort
let builder = {
\ 'type': 'shellcmd',
\ 'cmd': a:cmd,
\ 'execmd': a:execmd,
\ 'uri': a:uri,
\ 'use_vimproc': a:use_vimproc,
\}
function! builder.build() abort
" If args is not List, need to escape by open-browser
let args = deepcopy(self.cmd.args)
let need_escape = type(args) isnot type([])
let quote = need_escape ? "'" : ''
let expand_param = {
\ 'browser' : quote . self.execmd . quote,
\ 'browser_noesc': self.execmd,
\ 'uri' : quote . self.uri . quote,
\ 'uri_noesc' : self.uri,
\ 'use_vimproc' : self.use_vimproc,
\}
if type(args) is# type([])
let system_args = map(
\ copy(args), 's:_expand_keywords(v:val, expand_param)'
\)
else
let system_args = s:_expand_keywords(args, expand_param)
endif
return s:Opener.new_from_shellcmd(
\ system_args, get(self.cmd, 'background'), self.use_vimproc
\)
endfunction
return builder
endfunction
" If applicable browser is found, this returns s:O.some(builder) which
" builds shell command opener from given URI. Otherwise this returns
" s:O.none().
function! s:_get_shellcmd_opener_builder(uri, config) abort
let [uri, config] = [a:uri, a:config]
let use_vimproc = config.get('use_vimproc') && s:vimproc_is_installed
for cmd in config.get('browser_commands')
let execmd = get(cmd, 'cmd', cmd.name)
if executable(execmd)
let builder = s:_new_shellcmd_opener_builder(cmd, execmd, uri, use_vimproc)
return s:O.some(builder)
endif
endfor
return s:O.none()
endfunction
" :OpenBrowserSearch
function! s:_OpenBrowser_search(query, ...) abort dict
if a:query =~# '^\s*$'
return
endif
let default_search = self.config.get('default_search')
let engine = get(a:000, 0, default_search)
let engine = engine is# '' ? default_search : engine
let search_engines = self.config.get('search_engines')
if !has_key(search_engines, engine)
call s:Msg.error("Unknown search engine '" . engine . "'.")
return
endif
let query = s:_encodeURIComponent(a:query)
let uri = s:_expand_keywords(search_engines[engine], {'query': query})
call self.open(uri)
endfunction
" :OpenBrowserSmartSearch
function! s:_OpenBrowser_smart_search(query, ...) abort dict
let default_search = self.config.get('default_search')
let engine = get(a:000, 0, default_search)
let engine = engine is# '' ? default_search : engine
if s:_seems_path(a:query) || s:_valid_uri(s:URI.new(a:query, {}))
return self.open(a:query)
else
return self.search(a:query, engine)
endif
endfunction
function! s:_parse_spaces(cmdline) abort
return substitute(a:cmdline, '^\s\+', '', '')
endfunction
" Parse engine if specified
function! s:_parse_engine(cmdline) abort
let c = s:_parse_spaces(a:cmdline)
let engine = s:O.none()
let m = matchlist(c, '^-\(\S\+\)\s\+\(.*\)')
if !empty(m)
let engine = s:O.some(m[1])
let c = m[2]
endif
return [engine, c]
endfunction
" :OpenBrowser
function! s:_OpenBrowser_cmd_open(cmdline) abort dict
let uri = s:_parse_spaces(a:cmdline)
if uri is# ''
call s:Msg.error(':OpenBrowser {uri}')
return
endif
call self.open(uri)
endfunction
" :OpenBrowserSearch
function! s:_OpenBrowser_cmd_search(cmdline) abort dict
let [engine, c] = s:_parse_search_args(a:cmdline)
let c = s:_parse_spaces(c)
if c is# ''
call s:Msg.error(':OpenBrowserSearch [-{search-engine}] {query}')
return
endif
return self.search(c, s:O.get_or(engine, { -> '' }))
endfunction
" Parse command-line arguments of:
" * :OpenBrowserSearch
" * :OpenBrowserSmartSearch
function! s:_parse_search_args(cmdline) abort
let c = s:_parse_spaces(a:cmdline)
let engine = s:O.none()
while 1
if c =~# '^-'
let [engine, c] = s:_parse_engine(c)
if s:O.empty(engine)
break
endif
else
break
endif
endwhile
return [engine, c]
endfunction
" @vimlint(EVL103, 1, a:arglead)
" @vimlint(EVL103, 1, a:cursorpos)
function! s:_OpenBrowser_cmd_search_complete(arglead, cmdline, cursorpos) abort dict
let excmd = '^\s*OpenBrowser\w\+\s\+'
if a:cmdline !~# excmd
return
endif
let cmdline = substitute(a:cmdline, excmd, '', '')
let engine_opts = map(
\ sort(keys(self.config.get('search_engines'))),
\ '''-'' . v:val'
\)
let reg_opts = ['++clip', '++reg=']
let all_opts = engine_opts + reg_opts
if cmdline is# ''
return all_opts
endif
return filter(all_opts, 'stridx(v:val, cmdline) is# 0')
endfunction
" @vimlint(EVL103, 0, a:arglead)
" @vimlint(EVL103, 0, a:cursorpos)
" :OpenBrowserSmartSearch
function! s:_OpenBrowser_cmd_smart_search(cmdline) abort dict
let [engine, c] = s:_parse_search_args(a:cmdline)
let c = s:_parse_spaces(c)
if c is# ''
call s:Msg.error(':OpenBrowserSmartSearch [-{search-engine}] {query}')
return
endif
return self.smart_search(c, s:O.get_or(engine, { -> '' }))
endfunction
" <Plug>(openbrowser-open)
function! s:_OpenBrowser_keymap_open(mode, ...) abort dict
let silent = get(a:000, 0, self.config.get('message_verbosity') is# 0)
let options = get(a:000, 1, [])
if a:mode is# 'n'
" URL
let url = s:_get_url_on_cursor(self.config)
if !empty(url)
call self.open(url, options)
return 1
endif
" FilePath
let filepath = s:_get_filepath_on_cursor()
if !empty(filepath)
call self.open(filepath)
return 1
endif
" Fail!
if !silent
call s:Msg.error('URL or file path is not found under cursor!')
endif
return 0
else
let text = s:_get_selected_text()
let extracted = s:_extract_urls(text, self.config)
for e in extracted
call self.open(e.url.to_string())
endfor
return !empty(extracted)
endif
endfunction
" <Plug>(openbrowser-search)
function! s:_OpenBrowser_keymap_search(mode) abort dict
if a:mode is# 'n'
return self.search(expand('<cword>'))
else
return self.search(s:_get_selected_text())
endif
endfunction
" <Plug>(openbrowser-smart-search)
function! s:_OpenBrowser_keymap_smart_search(mode) abort dict
if self.keymap_open(a:mode, 1)
" Suceeded to open!
return
endif
" If neither URL nor FilePath is found...
if a:mode is# 'n'
" Search <cword>.
call self.search(
\ expand('<cword>'),
\ self.config.get('default_search'))
else
" Search selected text.
call self.search(
\ s:_get_selected_text(),
\ self.config.get('default_search'))
endif
endfunction
function! s:_get_selected_text() abort
let selected_text = s:_get_last_selected()
let text = substitute(selected_text, '[\n\r]\+', ' ', 'g')
return substitute(text, '^\s*\|\s*$', '', 'g')
endfunction
" Define more tolerant URI parsing.
" TODO: Make this configurable.
let s:LoosePatternSet = {}
function! s:_get_loose_pattern_set() abort
if !empty(s:LoosePatternSet)
return s:LoosePatternSet
endif
let s:LoosePatternSet = s:URI.new_default_pattern_set()
" Remove "'", "(", ")" from default sub_delims().
function! s:LoosePatternSet.sub_delims() abort
return '[!$&*+,;=]'
endfunction
return s:LoosePatternSet
endfunction
function! s:_extract_urls(text, config) abort
let pattern_set = s:_get_loose_pattern_set()
let schemes = keys(a:config.get('fix_schemes'))
let head_pattern = s:_get_url_head_pattern(schemes, pattern_set)
return s:URIExtractor.extract_from_text(a:text, {
\ 'uri_pattern_set': pattern_set,
\ 'head_pattern': head_pattern,
\})
endfunction
" This pattern matches:
" * "https", "http", "file", and the keys of Dictionary returned by
" config.get('fix_schemes')
" * Above 3 schemes are used because most frequently used, and URI scheme
" regex is too tolerant
" * URI hostname (for no scheme URI)
function! s:_get_url_head_pattern(schemes, pattern_set) abort
let schemes = ['https', 'http', 'file'] + a:schemes
let scheme_pattern = join(sort(copy(a:schemes), 's:_by_length'), '\|')
let head_pattern = scheme_pattern . '\|' . a:pattern_set.host()
return head_pattern
endfunction
function! s:_by_length(s1, s2) abort
let [l1, l2] = [strlen(a:s1), strlen(a:s2)]
return l1 ># l2 ? -1 : l1 <# l2 ? 1 : 0
endfunction
function! s:_seems_path(uri) abort
" - Has no invalid filename character (seeing &isfname)
" and, either
" - file:// prefixed string and existed file path
" - Existed file path
if stridx(a:uri, 'file://') is# 0
let path = substitute(a:uri, '^file://', '', '')
else
let path = a:uri
endif
return getftype(path) isnot# ''
endfunction
function! s:_valid_uri(uriobj) abort
return !empty(a:uriobj)
\ && a:uriobj.scheme() isnot# ''
endfunction
" @vimlint(EVL104, 1, l:save_shellslash)
function! s:_convert_to_fullpath(path) abort
if exists('+shellslash')
let save_shellslash = &l:shellslash
let &l:shellslash = 1
endif
try
return fnamemodify(a:path, ':p')
finally
if exists('+shellslash')
let &l:shellslash = save_shellslash
endif
endtry
endfunction
" @vimlint(EVL104, 0, l:save_shellslash)
function! s:_expand_format_message(format_message, keywords) abort
let maxlen = s:Msg.get_hit_enter_max_length()
let expanded_msg = s:_expand_keywords(a:format_message.msg, a:keywords)
if a:format_message.truncate && strlen(expanded_msg) > maxlen
" Avoid |hit-enter-prompt|.
let non_uri_len = strlen(expanded_msg) - strlen(a:keywords.uri)
" First Try: Remove "https" or "http" scheme in URI.
let scheme = '\C^https\?://'
let matched_len = strlen(matchstr(a:keywords.uri, scheme))
if matched_len > 0
let a:keywords.uri = a:keywords.uri[matched_len :]
endif
if non_uri_len + strlen(a:keywords.uri) <= maxlen
let expanded_msg = s:_expand_keywords(a:format_message.msg, a:keywords)
else
" Second Try: Even if expanded_msg is longer than command-line
" after "First Try", truncate URI as possible.
let min_uri_len = a:format_message.min_uri_len
if non_uri_len + min_uri_len <= maxlen
" Truncate only URI.
let a:keywords.uri = s:_truncate_skipping(
\ a:keywords.uri, maxlen - 4 - non_uri_len, 0, '...')
let expanded_msg = s:_expand_keywords(a:format_message.msg, a:keywords)
else
" Third, Fallback: Even if expanded_msg is longer than command-line
" after "Second Try", truncate whole string.
let a:keywords.uri = s:_truncate_skipping(
\ a:keywords.uri, min_uri_len, 0, '...')
let expanded_msg = s:_expand_keywords(a:format_message.msg, a:keywords)
let expanded_msg = s:_truncate_skipping(
\ expanded_msg, maxlen - 4, 0, '...')
endif
endif
endif
return expanded_msg
endfunction
" @return Dictionary: the URL on cursor, or the first URL after cursor
" Empty Dictionary means no URLs found.
" :help openbrowser-url-detection
function! s:_get_url_on_cursor(config) abort
let url = s:_get_thing_on_cursor('s:_detect_url_cb', [a:config])
return s:O.get_or(url, { -> '' })
endfunction
function! s:_detect_url_cb(config) abort
let extracted = s:_extract_urls(expand('<cWORD>'), a:config)
if !empty(extracted)
return s:O.some(extracted[0].url.to_string())
endif
return s:O.none()
endfunction
" @return the filepath on cursor, or the first filepath after cursor
" :help openbrowser-filepath-detection
function! s:_get_filepath_on_cursor() abort
let filepath = s:_get_thing_on_cursor('s:_detect_filepath_cb', [])
return s:O.get_or(filepath, { -> '' })
endfunction
function! s:_detect_filepath_cb() abort
let path = expand('<cWORD>')
return s:_seems_path(path) ? s:O.some(path) : s:O.none()
endfunction
function! s:_get_thing_on_cursor(func, args) abort
let line = s:_getconcealedline('.')
let col = s:_getconcealedcol('.')
if line[col-1] =~# '\s'
let pos = getpos('.')
try
" Search left WORD.
if search('\S', 'bnW')[0] ># 0
normal! B
let r = call(a:func, a:args)
if s:O.exists(r)
return r
endif
endif
" Search right WORD.
if search('\S', 'nW')[0] ># 0
normal! W
let r = call(a:func, a:args)
if s:O.exists(r)
return r
endif
endif
" Not found.
return s:O.none()
finally
call setpos('.', pos)
endtry
endif
return call(a:func, a:args)
endfunction
" This function is from quickrun.vim (http://github.com/thinca/vim-quickrun)
" Original function is `s:Runner.expand()`.
"
" NOTE: Original version recognize more keywords.
" This function is speciallized for open-browser.vim
" - @register @{register}
" - &option &{option}
" - $ENV_NAME ${ENV_NAME}
"
" Expand the keywords.
" - {expr}
"
" Escape by \ if you does not want to expand.
" - "\{keyword}" => "{keyword}", not expression `keyword`.
" it does not expand vim variable `keyword`.
function! s:_expand_keywords(str, options) abort
if type(a:str) isnot# type('') || type(a:options) isnot# type({})
call s:_throw('s:_expand_keywords(): invalid arguments. (a:str = '.string(a:str).', a:options = '.string(a:options).')')
endif
let rest = a:str
let result = ''
" Assign these variables for eval().
for [name, val] in items(a:options)
" unlockvar l:
" let l:[name] = val
execute 'let' name '=' string(val)
endfor
while 1
let f = match(rest, '\\\?[{]')
" No more special characters. end parsing.
if f < 0
let result .= rest
break
endif
" Skip ordinary string.
if f isnot# 0
let result .= rest[: f - 1]
let rest = rest[f :]
endif
" Process special string.
if rest[0] is# '\'
let result .= rest[1]
let rest = rest[2 :]
elseif rest[0] is# '{'
" NOTE: braindex + 1 is# 1, it skips first bracket (rest[0])
let braindex = 0
let braindex_stack = [braindex]
while !empty(braindex_stack)
let braindex = match(rest, '\\\@<![{}]', braindex + 1)
if braindex is# -1
call s:_throw('expression is invalid: curly bracket is not closed.')
elseif rest[braindex] is# '{'
call add(braindex_stack, braindex)
else " '}'
let brastart = remove(braindex_stack, -1)
" expr does not contain brackets.
" Assert: rest[brastart is# '{' && rest[braindex] is# '}'
let left = brastart is# 0 ? '' : rest[: brastart-1]
let expr = rest[brastart+1 : braindex-1]
let right = rest[braindex+1 :]
" Remove(unescape) backslashes.
let expr = substitute(expr, '\\\([{}]\)', '\1', 'g')
let value = eval(expr) . ''
let rest = left . value . right
let braindex -= len(expr) - len(value)
endif
endwhile
let result .= rest[: braindex]
let rest = rest[braindex+1 :]
else
call s:_throw('parse error: rest = ' . rest . ', result = ' . result)
endif
endwhile
return result
endfunction
function! s:_throw(msg) abort
throw 'openbrowser: ' . a:msg
endfunction
" From https://github.com/chikatoike/concealedyank.vim
function! s:_getconcealedline(lnum, ...) abort
if !has('conceal')
return getline(a:lnum)
endif
let line = getline(a:lnum)
let index = get(a:000, 0, 0)
let endidx = get(a:000, 1, -1)
let endidx = endidx >= 0 ? min([endidx, strlen(line)]) : strlen(line)
let region = -1
let ret = ''
while index <= endidx
let concealed = synconcealed(a:lnum, index + 1)
if concealed[0] isnot# 0
if region isnot# concealed[2]
let region = concealed[2]
let ret .= concealed[1]
endif
else
let ret .= line[index]
endif
" get next char index.
let index += 1
endwhile
return ret
endfunction
function! s:_getconcealedcol(expr) abort
if !has('conceal')
return col(a:expr)
endif
let index = 0
let endidx = col(a:expr)
let ret = 0
let isconceal = 0
while index < endidx
let concealed = synconcealed('.', index + 1)
if concealed[0] is# 0
let ret += 1
endif
let isconceal = concealed[0]
" get next char index.
let index += 1
endwhile
if ret is# 0
let ret = 1
elseif isconceal
let ret += 1
endif
return ret
endfunction
let &cpo = s:save_cpo