mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-03 21:00:06 +08:00
777 lines
22 KiB
VimL
777 lines
22 KiB
VimL
" 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
|