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(''), ':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(''), '\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, '"', '"', 'g') let s = substitute(s, ''', "'", 'g') let s = substitute(s, '>', '>', 'g') let s = substitute(s, '<', '<', 'g') return substitute(s, '&', '\&', '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