1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 12:50:04 +08:00
SpaceVim/bundle/vim-grammarous/autoload/grammarous.vim

799 lines
23 KiB
VimL
Raw Permalink Normal View History

let s:save_cpo = &cpo
set cpo&vim
let s:V = vital#grammarous#new()
let s:XML = s:V.import('Web.XML')
let s:O = s:V.import('OptionParser')
let s:P = s:V.import('Process')
let s:is_cygwin = has('win32unix')
let s:is_windows = has('win32') || has('win64')
let s:job_is_available = has('job') && has('patch-8.0.0027')
let s:grammarous_root = fnamemodify(expand('<sfile>'), ':p:h:h')
let g:grammarous#jar_dir = get(g:, 'grammarous#jar_dir', s:grammarous_root . '/misc')
let g:grammarous#jar_url = get(g:, 'grammarous#jar_url', 'https://www.languagetool.org/download/LanguageTool-5.9.zip')
let g:grammarous#java_cmd = get(g:, 'grammarous#java_cmd', 'java')
let g:grammarous#default_lang = get(g:, 'grammarous#default_lang', 'en')
let g:grammarous#use_vim_spelllang = get(g:, 'grammarous#use_vim_spelllang', 0)
let g:grammarous#info_window_height = get(g:, 'grammarous#info_window_height', 10)
let g:grammarous#info_win_direction = get(g:, 'grammarous#info_win_direction', 'botright')
let g:grammarous#use_fallback_highlight = get(g:, 'grammarous#use_fallback_highlight', !exists('*matchaddpos'))
let g:grammarous#enabled_rules = get(g:, 'grammarous#enabled_rules', {})
let g:grammarous#disabled_rules = get(g:, 'grammarous#disabled_rules', {'*' : ['WHITESPACE_RULE', 'EN_QUOTES']})
let g:grammarous#enabled_categories = get(g:, 'grammarous#enabled_categories', {})
let g:grammarous#disabled_categories = get(g:, 'grammarous#disabled_categories', {})
let g:grammarous#default_comments_only_filetypes = get(g:, 'grammarous#default_comments_only_filetypes', {'*' : 0})
let g:grammarous#enable_spell_check = get(g:, 'grammarous#enable_spell_check', 0)
let g:grammarous#move_to_first_error = get(g:, 'grammarous#move_to_first_error', 1)
let g:grammarous#hooks = get(g:, 'grammarous#hooks', {})
let g:grammarous#languagetool_cmd = get(g:, 'grammarous#languagetool_cmd', '')
let g:grammarous#show_first_error = get(g:, 'grammarous#show_first_error', 0)
let g:grammarous#use_location_list = get(g:, 'grammarous#use_location_list', 0)
highlight default link GrammarousError SpellBad
highlight default link GrammarousInfoError ErrorMsg
highlight default link GrammarousInfoSection Keyword
highlight default link GrammarousInfoHelp Special
augroup pluging-rammarous-highlight
autocmd ColorScheme * highlight default link GrammarousError SpellBad
autocmd ColorScheme * highlight default link GrammarousInfoError ErrorMsg
autocmd ColorScheme * highlight default link GrammarousInfoSection Keyword
autocmd ColorScheme * highlight default link GrammarousInfoHelp Special
augroup END
function! s:get_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\d\+_\zeget_SID$')
endfunction
let s:SID = s:get_SID()
delfunction s:get_SID
function! grammarous#_import_vital_modules()
return [s:XML, s:O, s:P]
endfunction
function! grammarous#error(...)
echohl ErrorMsg
try
if a:0 > 1
let msg = 'vim-grammarous: ' . call('printf', a:000)
else
let msg = 'vim-grammarous: ' . a:1
endif
for l in split(msg, "\n")
echomsg l
endfor
finally
echohl None
endtry
endfunction
function! s:delete_jar_dir() abort
if !isdirectory(g:grammarous#jar_dir)
return
endif
let dir = g:grammarous#jar_dir
if s:is_cygwin
let dir = s:cygpath(dir)
endif
if dir ==# '' || !isdirectory(dir)
call grammarous#error("Directory '%s' does not exist", dir)
return
endif
if s:is_windows && !s:is_cygwin
let cmd = 'rmdir /s /q ' . dir
else
let cmd = 'rm -rf ' . dir
endif
let out = system(cmd)
if v:shell_error
call grammarous#error("Cannot remove the directory '%s': %s", dir, out)
return
endif
echomsg 'Deleted ' . dir
unlet! s:jar_file
endfunction
function! s:find_jar(dir)
return findfile('languagetool-commandline.jar', a:dir . '/**')
endfunction
function! s:prepare_jar(dir)
let jar = s:find_jar(a:dir)
if jar ==# ''
if grammarous#downloader#download(a:dir)
let jar = s:find_jar(a:dir)
endif
endif
return fnamemodify(jar, ':p')
endfunction
function! s:find_jar_path()
if exists('s:jar_file')
return s:jar_file
endif
if !executable(g:grammarous#java_cmd)
call grammarous#error('"java" command not found. Please install Java 8+')
return ''
endif
" TODO:
" Check java version
let jar = s:prepare_jar(g:grammarous#jar_dir)
if jar ==# ''
call grammarous#error('Failed to get LanguageTool')
return ''
endif
if s:is_cygwin
let jar = s:cygpath(jar)
endif
let s:jar_file = jar
return jar
endfunction
function! s:cygpath(path) abort
if !executable('cygpath')
return a:path
endif
" On Cygwin environment, paths should be converted with cygpath.
" /cygdrive/c/... -> C:/...
" https://github.com/rhysd/vim-grammarous/issues/30
let converted = substitute(s:P.system('cygpath -aw ' . a:path), '\n\+$', '', '')
if s:P.get_last_status()
return a:path
endif
return converted
endfunction
function! s:make_text(text)
if type(a:text) == type('')
return a:text
else
return join(a:text, "\n")
endif
endfunction
function! s:set_errors_to_location_list() abort
let f = expand('%:p')
let saved_efm = &l:errorformat
try
setlocal errorformat=%f:%l:%c:%m
let lines = map(copy(b:grammarous_result), '
\ printf("%s:%s:%s:%s [%s]", f, v:val.fromy + 1, v:val.fromx + 1, v:val.msg, v:val.category)
\')
lgetexpr lines
finally
let &l:errorformat = saved_efm
endtry
endfunction
function! s:set_errors_from_xml_string(xml) abort
let b:grammarous_result = grammarous#get_errors_from_xml(s:XML.parse(substitute(a:xml, "\n", '', 'g')))
let parsed = s:last_parsed_options
if s:is_comment_only(parsed['comments-only'])
call filter(b:grammarous_result, 'synIDattr(synID(v:val.fromy+1, v:val.fromx+1, 0), "name") =~? "comment"')
endif
redraw!
if empty(b:grammarous_result)
echomsg 'Yay! No grammatical errors detected.'
return
endif
let len = len(b:grammarous_result)
echomsg printf('Detected %d grammatical error%s', len, len > 1 ? 's' : '')
call grammarous#highlight_errors_in_current_buffer(b:grammarous_result)
if parsed['move-to-first-error']
call cursor(b:grammarous_result[0].fromy+1, b:grammarous_result[0].fromx+1)
endif
if g:grammarous#enable_spell_check
let s:saved_spell = &l:spell
setlocal spell
endif
if g:grammarous#use_location_list
call s:set_errors_to_location_list()
endif
if g:grammarous#show_first_error
call grammarous#create_update_info_window_of(b:grammarous_result)
endif
if has_key(g:grammarous#hooks, 'on_check')
call call(g:grammarous#hooks.on_check, [b:grammarous_result], g:grammarous#hooks)
endif
endfunction
function! s:on_check_done_vim8(channel) abort
let xml = ''
while ch_status(a:channel, {'part' : 'out'}) ==# 'buffered'
let xml .= ch_read(a:channel)
endwhile
if xml ==# ''
return
endif
call s:set_errors_from_xml_string(xml)
endfunction
function! s:on_check_exit_vim8(channel, status) abort
if a:status == 0
return
endif
let err = ''
while ch_status(a:channel, {'part' : 'err'}) ==# 'buffered'
let err .= ch_read(a:channel, {'part' : 'err'})
endwhile
call grammarous#error('Grammar check failed with exit status ' . a:status . ': ' . err)
endfunction
function! s:on_exit_nvim(job, status, event) abort dict
if a:status != 0
call grammarous#error('Grammar check failed: ' . self._stderr)
return
endif
call s:set_errors_from_xml_string(self._stdout)
endfunction
function! s:on_output_nvim(job, lines, event) abort dict
let output = join(a:lines, "\n")
if a:event ==# 'stdout'
let self._stdout .= output
else
let self._stderr .= output
endif
endfunction
function! s:invoke_check(range_start, ...)
if g:grammarous#languagetool_cmd ==# ''
let jar = s:find_jar_path()
if jar ==# ''
return
endif
endif
if a:0 < 1
call grammarous#error('Invalid argument. At least one argument is required.')
return
endif
if g:grammarous#use_vim_spelllang
" Convert vim spelllang to languagetool spelllang
if len(split(&spelllang, '_')) == 1
let lang = split(&spelllang, '_')[0]
elseif len(split(&spelllang, '_')) == 2
let lang = split(&spelllang, '_')[0].'-'.toupper(split(&spelllang, '_')[1])
endif
else
let lang = a:0 == 1 ? g:grammarous#default_lang : a:1
endif
let text = s:make_text(a:0 == 1 ? a:1 : a:2)
let tmpfile = tempname()
execute 'redir! >' tmpfile
let l = 1
while l < a:range_start
silent echo ""
let l += 1
endwhile
silent echon text
redir END
if s:is_cygwin
let tmpfile = s:cygpath(tmpfile)
endif
let cmdargs = printf(
\ '-c %s -l %s --api %s',
\ &fileencoding ? &fileencoding : &encoding,
\ lang,
\ substitute(tmpfile, '\\\s\@!', '\\\\', 'g')
\ )
let disabled_rules = get(g:grammarous#disabled_rules, &filetype, get(g:grammarous#disabled_rules, '*', []))
if !empty(disabled_rules)
let cmdargs = '-d ' . join(disabled_rules, ',') . ' ' . cmdargs
endif
let enabled_rules = get(g:grammarous#enabled_rules, &filetype, get(g:grammarous#enabled_rules, '*', []))
if !empty(enabled_rules)
let cmdargs = '-e ' . join(enabled_rules, ',') . ' ' . cmdargs
endif
let disabled_categories = get(g:grammarous#disabled_categories, &filetype, get(g:grammarous#disabled_categories, '*', []))
if !empty(disabled_categories)
let cmdargs = '--disablecategories ' . join(disabled_categories, ',') . ' ' . cmdargs
endif
let enabled_categories = get(g:grammarous#enabled_categories, &filetype, get(g:grammarous#enabled_categories, '*', []))
if !empty(enabled_categories)
let cmdargs = '--enablecategories ' . join(enabled_categories, ',') . ' ' . cmdargs
endif
if g:grammarous#languagetool_cmd !=# ''
let cmd = printf('%s %s', g:grammarous#languagetool_cmd, cmdargs)
else
let cmd = printf('%s -jar %s %s', g:grammarous#java_cmd, substitute(jar, '\\\s\@!', '\\\\', 'g'), cmdargs)
endif
if s:job_is_available
let job = job_start(cmd, {'close_cb' : s:SID . 'on_check_done_vim8', 'exit_cb' : s:SID . 'on_check_exit_vim8'})
echo 'Grammar check has started with job(' . job . ')...'
return
endif
if has('nvim')
let opts = {
\ 'on_stdout' : function('s:on_output_nvim'),
\ 'on_stderr' : function('s:on_output_nvim'),
\ 'on_exit' : function('s:on_exit_nvim'),
\ '_stdout' : '',
\ '_stderr' : '',
\ }
let job = jobstart(cmd, opts)
echo 'Grammar check has started with job(id: ' . job . ')...'
return
endif
let xml = s:P.system(cmd)
call delete(tmpfile)
if s:P.get_last_status()
call grammarous#error("Command '%s' failed:\n%s", cmd, xml)
return
endif
call s:set_errors_from_xml_string(xml)
endfunction
function! s:sanitize(s)
return substitute(escape(a:s, "'\\"), ' ', '\\_\\s', 'g')
endfunction
function! grammarous#generate_highlight_pattern(error)
let line = a:error.fromy + 1
let prefix = a:error.contextoffset > 0 ? s:sanitize(a:error.context[: a:error.contextoffset-1]) : ''
let rest = a:error.context[a:error.contextoffset :]
let the_error = s:sanitize(rest[: a:error.errorlength-1])
let rest = s:sanitize(rest[a:error.errorlength :])
return '\V' . prefix . '\zs' . the_error . '\ze' . rest
endfunction
function! s:unescape_xml(str)
let s = substitute(a:str, '&quot;', '"', 'g')
let s = substitute(s, '&apos;', "'", 'g')
let s = substitute(s, '&gt;', '>', 'g')
let s = substitute(s, '&lt;', '<', 'g')
return substitute(s, '&amp;', '\&', 'g')
endfunction
function! s:unescape_error(err)
for e in ['context', 'msg', 'replacements']
let a:err[e] = s:unescape_xml(a:err[e])
endfor
return a:err
endfunction
function! grammarous#get_errors_from_xml(xml)
return map(filter(a:xml.childNodes(), 'v:val.name ==# "error"'), 's:unescape_error(v:val.attr)')
endfunction
function! s:matcherrpos(...)
return matchaddpos('GrammarousError', [a:000], 999)
endfunction
function! s:highlight_error(from, to)
if a:from[0] == a:to[0]
return s:matcherrpos(a:from[0], a:from[1], a:to[1] - a:from[1])
endif
let ids = [s:matcherrpos(a:from[0], a:from[1], strlen(getline(a:from[0]))+1 - a:from[1])]
let line = a:from[0] + 1
while line < a:to[0]
call add(ids, s:matcherrpos(line))
let line += 1
endwhile
call add(ids, s:matcherrpos(a:to[0], 1, a:to[1] - 1))
return ids
endfunction
function! s:remove_3dots(str)
return substitute(substitute(a:str, '\.\.\.$', '', ''), '\\V\zs\.\.\.', '', '')
endfunction
function! grammarous#highlight_errors_in_current_buffer(errs)
if !g:grammarous#use_fallback_highlight
for e in a:errs
let e.id = s:highlight_error(
\ [str2nr(e.fromy)+1, str2nr(e.fromx)+1],
\ [str2nr(e.toy)+1, str2nr(e.tox)+1],
\ )
endfor
else
for e in a:errs
let e.id = matchadd(
\ 'GrammarousError',
\ s:remove_3dots(grammarous#generate_highlight_pattern(e)),
\ 999
\ )
endfor
endif
endfunction
function! grammarous#reset_highlights()
for m in filter(getmatches(), 'v:val.group ==# "GrammarousError"')
call matchdelete(m.id)
endfor
endfunction
function! grammarous#find_checked_winnr() abort
if exists('b:grammarous_result')
return winnr()
endif
for bufnr in tabpagebuflist()
let result = getbufvar(bufnr, 'grammarous_result', [])
if empty(result)
continue
endif
let winnr = bufwinnr(bufnr)
if winnr == -1
continue
endif
return winnr
endfor
return -1
endfunction
function! grammarous#reset()
let win = grammarous#find_checked_winnr()
if win == -1
return
endif
let prev_win = winnr()
if win != prev_win
execute win . 'wincmd w'
endif
if g:grammarous#use_location_list
lclose
lgetexpr []
endif
call grammarous#reset_highlights()
call grammarous#info_win#stop_auto_preview()
call grammarous#info_win#close()
if exists('s:saved_spell')
let &l:spell = s:saved_spell
unlet s:saved_spell
endif
if has_key(g:grammarous#hooks, 'on_reset')
call call(g:grammarous#hooks.on_reset, [b:grammarous_result], g:grammarous#hooks)
endif
unlet! b:grammarous_result b:grammarous_preview_bufnr
if win != prev_win
wincmd p
endif
endfunction
let s:opt_parser = s:O.new()
\.on('--lang=VALUE', 'language to check', {'default' : g:grammarous#default_lang})
\.on('--[no-]preview', 'enable auto preview', {'default' : 1})
\.on('--[no-]comments-only', 'check comment only', {'default' : ''})
\.on('--[no-]move-to-first-error', 'move to first error', {'default' : g:grammarous#move_to_first_error})
\.on('--reinstall-languagetool', 'reinstall LanguageTool', {'default' : 0})
function! grammarous#complete_opt(arglead, cmdline, cursorpos)
return s:opt_parser.complete(a:arglead, a:cmdline, a:cursorpos)
endfunction
function! s:is_comment_only(option)
if type(a:option) == type(0)
return a:option
endif
return get(
\ g:grammarous#default_comments_only_filetypes,
\ &filetype,
\ get(g:grammarous#default_comments_only_filetypes, '*', 0)
\ )
endfunction
function! grammarous#check_current_buffer(qargs, range)
if exists('b:grammarous_result')
call grammarous#reset()
redraw!
endif
let parsed = s:opt_parser.parse(a:qargs, a:range, '')
if has_key(parsed, 'help')
return
endif
let b:grammarous_auto_preview = parsed.preview
if parsed.preview
call grammarous#info_win#start_auto_preview()
endif
if parsed['reinstall-languagetool']
call s:delete_jar_dir()
endif
" XXX
let s:last_parsed_options = parsed
call s:invoke_check(
\ parsed.__range__[0],
\ parsed.lang,
\ getline(parsed.__range__[0], parsed.__range__[1])
\ )
endfunction
function! s:less_position(p1, p2)
if a:p1[0] != a:p2[0]
return a:p1[0] < a:p2[0]
endif
return a:p1[1] < a:p2[1]
endfunction
function! s:binary_search_by_pos(errors, the_pos, start, end)
if a:start > a:end
return {}
endif
let m = (a:start + a:end) / 2
let from = [a:errors[m].fromy+1, a:errors[m].fromx+1]
let to = [a:errors[m].toy+1, a:errors[m].tox]
if s:less_position(a:the_pos, from)
return s:binary_search_by_pos(a:errors, a:the_pos, a:start, m-1)
endif
if s:less_position(to, a:the_pos)
return s:binary_search_by_pos(a:errors, a:the_pos, m+1, a:end)
endif
return a:errors[m]
endfunction
" Note:
" It believes all errors are sorted by its position
function! grammarous#get_error_at(pos, errs)
return s:binary_search_by_pos(a:errs, a:pos, 0, len(a:errs)-1)
endfunction
function! grammarous#fixit(err)
if empty(a:err)
\ || !grammarous#move_to_checked_buf(a:err.fromy+1, a:err.fromx+1)
\ || a:err.replacements ==# ''
call grammarous#error('Cannot fix this error automatically.')
return
endif
let sel_save = &l:selection
let &l:selection = 'inclusive'
let save_g_reg = getreg('g', 1)
let save_g_regtype = getregtype('g')
try
normal! v
call cursor(a:err.toy+1, a:err.tox)
noautocmd normal! "gy
let from = getreg('g')
let to = split(a:err.replacements, '#', 1)[0]
call setreg('g', to, 'v')
normal! gv"gp
call grammarous#remove_error(a:err, get(a:, 1, b:grammarous_result))
echomsg printf("Fixed: '%s' -> '%s'", from, to)
finally
call setreg('g', save_g_reg, save_g_regtype)
let &l:selection = sel_save
endtry
endfunction
function! grammarous#fixall(errs)
for e in a:errs
call grammarous#fixit(e)
endfor
endfunction
function! s:move_to_pos(pos)
let p = type(a:pos[0]) == type([]) ? a:pos[0] : a:pos
return cursor(a:pos[0], a:pos[1]) != -1
endfunction
function! s:move_to(buf, pos)
if a:buf != bufnr('%')
let winnr = bufwinnr(a:buf)
if winnr == -1
return 0
endif
execute winnr . 'wincmd w'
endif
return s:move_to_pos(a:pos)
endfunction
function! grammarous#move_to_checked_buf(...)
if exists('b:grammarous_result')
return s:move_to_pos(a:000)
endif
if exists('b:grammarous_preview_original_bufnr')
return s:move_to(b:grammarous_preview_original_bufnr, a:000)
endif
for b in tabpagebuflist()
if !empty(getbufvar(b, 'grammarous_result', []))
return s:move_to(b, a:000)
endif
endfor
return 0
endfunction
function! grammarous#create_update_info_window_of(errs)
let e = grammarous#get_error_at(getpos('.')[1 : 2], a:errs)
if empty(e)
return
endif
if exists('b:grammarous_preview_bufnr')
let winnr = bufwinnr(b:grammarous_preview_bufnr)
if winnr == -1
let bufnr = grammarous#info_win#open(e, bufnr('%'))
else
execute winnr . 'wincmd w'
let bufnr = grammarous#info_win#update(e)
endif
else
let bufnr = grammarous#info_win#open(e, bufnr('%'))
endif
wincmd p
let b:grammarous_preview_bufnr = bufnr
endfunction
function! grammarous#create_and_jump_to_info_window_of(errs)
call grammarous#create_update_info_window_of(a:errs)
wincmd p
endfunction
function! s:remove_error_highlight(e)
let ids = type(a:e.id) == type([]) ? a:e.id : [a:e.id]
for i in ids
silent! if matchdelete(i) == -1
return 0
endif
endfor
return 1
endfunction
function! grammarous#remove_error(e, errs)
if !s:remove_error_highlight(a:e)
return 0
endif
for i in range(len(a:errs))
if type(a:errs[i].id) == type(a:e.id) && a:errs[i].id == a:e.id
call grammarous#info_win#close()
unlet a:errs[i]
return 1
endif
endfor
return 0
endfunction
function! grammarous#remove_error_at(pos, errs)
let e = grammarous#get_error_at(a:pos, a:errs)
if empty(e)
return 0
endif
return grammarous#remove_error(e, a:errs)
endfunction
function! grammarous#disable_rule(rule, errs)
call grammarous#info_win#close()
" Note:
" reverse() is needed because of removing elements in list
for i in reverse(range(len(a:errs)))
let e = a:errs[i]
if e.ruleId ==# a:rule
if !s:remove_error_highlight(e)
return 0
endif
unlet a:errs[i]
endif
endfor
echomsg 'Disabled rule: ' . a:rule
return 1
endfunction
function! grammarous#disable_rule_at(pos, errs)
let e = grammarous#get_error_at(a:pos, a:errs)
if empty(e)
return 0
endif
return grammarous#disable_rule(e.ruleId, a:errs)
endfunction
function! grammarous#disable_category(category, errs)
call grammarous#info_win#close()
" Note:
" reverse() is needed because of removing elements in list
for i in reverse(range(len(a:errs)))
let e = a:errs[i]
if e.categoryid ==# a:category
if !s:remove_error_highlight(e)
return 0
endif
unlet a:errs[i]
endif
endfor
echomsg 'Disabled category: ' . a:category
return 1
endfunction
function! grammarous#disable_category_at(pos, errs)
let e = grammarous#get_error_at(a:pos, a:errs)
if empty(e)
return 0
endif
return grammarous#disable_category(e.categoryid, a:errs)
endfunction
function! grammarous#move_to_next_error(pos, errs)
for e in a:errs
let p = [e.fromy+1, e.fromx+1]
if s:less_position(a:pos, p)
return s:move_to_pos(p)
endif
endfor
call grammarous#error('No next error found.')
return 0
endfunction
function! grammarous#move_to_previous_error(pos, errs)
for e in reverse(copy(a:errs))
let p = [e.fromy+1, e.fromx+1]
if s:less_position(p, a:pos)
return s:move_to_pos(p)
endif
endfor
call grammarous#error('No previous error found.')
return 0
endfunction
let &cpo = s:save_cpo
unlet s:save_cpo