From 11c11e578f22cda52048fdec1354f9675b413495 Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 9 Sep 2016 00:23:26 +0100 Subject: [PATCH] Add linting with eslint in NeoVim, with a few bugs. --- .gitignore | 1 + LICENSE | 22 ++++ README.md | 18 +++ ale_linters/javascript/eslint.vim | 41 +++++++ plugin/ale/aaflags.vim | 42 +++++++ plugin/ale/cursor.vim | 72 ++++++++++++ plugin/ale/sign.vim | 42 +++++++ plugin/ale/zmain.vim | 182 ++++++++++++++++++++++++++++++ 8 files changed, 420 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 ale_linters/javascript/eslint.vim create mode 100644 plugin/ale/aaflags.vim create mode 100644 plugin/ale/cursor.vim create mode 100644 plugin/ale/sign.vim create mode 100644 plugin/ale/zmain.vim diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7f499f80 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/init.vim diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a8162f2e --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2016, w0rp +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..00720e4a --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# ALE - Asynchronous Lint Engine + +ALE (Asynchronous Lint Engine) is a plugin for providing linting in NeoVim +and Vim 8 while you edit your text files. + +ALE makes use of NeoVim and Vim 8 job control functions and timers to +run linters on the contents of text buffers and return errors as +text is changed in Vim. This allows for displaying warnings and +errors in files being edited in Vim before file has been saved +back to disk. + +**NOTE:** This Vim plugin has been written pretty quickly so far, +and is still in rapid development. Documentation and stable APIs will +follow later. + +## Known Bugs + +1. Warnings are cleared when a syntax error is hit with eslint. diff --git a/ale_linters/javascript/eslint.vim b/ale_linters/javascript/eslint.vim new file mode 100644 index 00000000..d6763eef --- /dev/null +++ b/ale_linters/javascript/eslint.vim @@ -0,0 +1,41 @@ +if exists('g:loaded_ale_linters_javascript_eslint') + finish +endif + +let g:loaded_ale_linters_javascript_eslint = 1 + +function! ale_linters#javascript#eslint#Handle(lines) + " Matches patterns line the following: + " + " :47:14: Missing trailing comma. [Warning/comma-dangle] + " :56:41: Missing semicolon. [Error/semi] + let pattern = '^:\(\d\+\):\(\d\+\): \(.\+\) \[\(.\+\)/\(.\+\)\]' + let output = [] + + for line in a:lines + let match = matchlist(line, pattern) + + if len(match) == 0 + break + endif + + " vcol is Needed to indicate that the column is a character. + call add(output, { + \ 'bufnr': bufnr('%'), + \ 'lnum': match[1] + 0, + \ 'vcol': 0, + \ 'col': match[2] + 0, + \ 'text': match[3] . '(' . match[5] . ')', + \ 'type': match[4] ==# 'Warning' ? 'W' : 'E', + \ 'nr': -1, + \}) + endfor + + return output +endfunction + +call ALEAddLinter('javascript', { +\ 'executable': 'eslint', +\ 'command': 'eslint -f unix --stdin', +\ 'callback': 'ale_linters#javascript#eslint#Handle', +\}) diff --git a/plugin/ale/aaflags.vim b/plugin/ale/aaflags.vim new file mode 100644 index 00000000..70021a6e --- /dev/null +++ b/plugin/ale/aaflags.vim @@ -0,0 +1,42 @@ +" This file sets up configuration settings for the ALE plugin. +" Flags can be set in vimrc files and so on to disable particular features, +" etc. + +if exists('g:loaded_ale_flags') + finish +endif + +let g:loaded_ale_flags = 1 + +" This flag can be set to 0 to disable linting when text is changed. +if !exists('g:ale_lint_on_text_changed') + let g:ale_lint_on_text_changed = 1 +endif + +" This flag can be set with a number of milliseconds for delaying the +" execution of a linter when text is changed. The timeout will be set and +" cleared each time text is changed, so repeated edits won't trigger the +" jobs for linting until enough time has passed after editing is done. +if !exists('g:ale_lint_delay') + let g:ale_lint_delay = 100 +endif + +" This flag can be set to 0 to disable linting when the buffer is entered. +if !exists('g:ale_lint_on_enter') + let g:ale_lint_on_enter = 1 +endif + +" This flag can be set to 0 to disable setting the loclist. +if !exists('g:ale_set_loclist') + let g:ale_set_loclist = 1 +endif + +" This flag can be set to 0 to disable setting signs. +if !exists('g:ale_set_signs') + let g:ale_set_signs = 1 +endif + +" This flag can be set to 0 to disable echoing when the cursor moves. +if !exists('g:ale_echo_cursor') + let g:ale_echo_cursor = 1 +endif diff --git a/plugin/ale/cursor.vim b/plugin/ale/cursor.vim new file mode 100644 index 00000000..0159302a --- /dev/null +++ b/plugin/ale/cursor.vim @@ -0,0 +1,72 @@ +if exists('g:loaded_ale_cursor') + finish +endif + +let g:loaded_ale_cursor = 1 + +" This function will perform a binary search to find a message from the +" loclist to echo when the cursor moves. +function! s:BinarySearch(loclist, line, column) + let min = 0 + let max = len(a:loclist) - 1 + let last_column_match = -1 + + while 1 + if max < min + return last_column_match + endif + + let mid = (min + max) / 2 + let obj = a:loclist[mid] + + " Binary search to get on the same line + if a:loclist[mid]['lnum'] < a:line + let min = mid + 1 + elseif a:loclist[mid]['lnum'] > a:line + let max = mid - 1 + else + let last_column_match = mid + + " Binary search to get the same column, or near it + if a:loclist[mid]['col'] < a:column + let min = mid + 1 + elseif a:loclist[mid]['col'] > a:column + let max = mid - 1 + else + return mid + endif + endif + endwhile +endfunction + +function! ale#cursor#TruncatedEcho(message) + let message = a:message + " Change tabs to spaces. + let message = substitute(message, "\t", ' ', 'g') + " Remove any newlines in the message. + let message = substitute(message, "\n", '', 'g') + + let truncated_message = join(split(message, '\zs')[:&columns - 2], '') + + " Echo the message truncated to fit without creating a prompt. + echo truncated_message +endfunction + +function! ale#cursor#EchoCursorWarning() + let pos = getcurpos() + + let index = s:BinarySearch(b:ale_loclist, pos[1], pos[2]) + + if index >= 0 + call ale#cursor#TruncatedEcho(b:ale_loclist[index]['text']) + else + echo + endif +endfunction + +if g:ale_echo_cursor + augroup ALECursorGroup + autocmd! + autocmd CursorMoved * call ale#cursor#EchoCursorWarning() + augroup END +endif diff --git a/plugin/ale/sign.vim b/plugin/ale/sign.vim new file mode 100644 index 00000000..29753011 --- /dev/null +++ b/plugin/ale/sign.vim @@ -0,0 +1,42 @@ +if exists('g:loaded_ale_sign') + finish +endif + +let g:loaded_ale_sign = 1 + +if !hlexists('ALEErrorSign') + highlight link ALErrorSign error +endif + +if !hlexists('ALEWarningSign') + highlight link ALEWarningSign todo +endif + +if !hlexists('ALEError') + highlight link ALEError SpellBad +endif + +if !hlexists('ALEWarning') + highlight link ALEWarning SpellCap +endif + +" Signs show up on the left for error markers. +sign define ALEErrorSign text=>> texthl=ALEErrorSign +sign define ALEWarningSign text=-- texthl=ALEWarningSign + +" This function will set the signs which show up on the left. +function! ale#sign#SetSigns(loclist) + sign unplace * + + for i in range(0, len(a:loclist) - 1) + let obj = a:loclist[i] + let name = obj['type'] ==# 'W' ? 'ALEWarningSign' : 'ALEErrorSign' + + let sign_line = 'sign place ' . (i + 1) + \. ' line=' . obj['lnum'] + \. ' name=' . name + \. ' buffer=' . obj['bufnr'] + + exec sign_line + endfor +endfunction diff --git a/plugin/ale/zmain.vim b/plugin/ale/zmain.vim new file mode 100644 index 00000000..2db0d028 --- /dev/null +++ b/plugin/ale/zmain.vim @@ -0,0 +1,182 @@ +" Always set buffer variables for each buffer +let b:ale_should_reset_loclist = 0 +let b:ale_loclist = [] + +if exists('g:loaded_ale_zmain') + finish +endif + +let g:loaded_ale_zmain = 1 + +let s:lint_timer = -1 +let s:linters = {} +let s:job_linter_map = {} +let s:job_output_map = {} + +function! s:ClearJob(job) + if a:job != -1 + let linter = s:job_linter_map[a:job] + + call jobstop(a:job) + call remove(s:job_output_map, a:job) + call remove(s:job_linter_map, a:job) + + let linter.job = -1 + endif +endfunction + +function! s:GatherOutput(job, data, event) + if !has_key(s:job_output_map, a:job) + return + endif + + call extend(s:job_output_map[a:job], a:data) +endfunction + +function! s:LocItemCompare(left, right) + if a:left['lnum'] < a:right['lnum'] + return -1 + endif + + if a:left['lnum'] > a:right['lnum'] + return 1 + endif + + if a:left['col'] < a:right['col'] + return -1 + endif + + if a:left['col'] > a:right['col'] + return 1 + endif + + return 0 +endfunction + +function! s:HandleExit(job, data, event) + if !has_key(s:job_linter_map, a:job) + return + endif + + let linter = s:job_linter_map[a:job] + let output = s:job_output_map[a:job] + + call s:ClearJob(a:job) + + let linter_loclist = function(linter.callback)(output) + + if b:ale_should_reset_loclist + let b:ale_should_reset_loclist = 0 + let b:ale_loclist = [] + endif + + " Add the loclist items from the linter. + call extend(b:ale_loclist, linter_loclist) + + " Sort the loclist again. + " We need a sorted list so we can run a binary search against it + " for efficient lookup of the messages in the cursor handler. + call sort(b:ale_loclist, 's:LocItemCompare') + + if g:ale_set_loclist + call setloclist(0, b:ale_loclist) + endif + + if g:ale_set_signs + call ale#sign#SetSigns(b:ale_loclist) + endif + + " Mark line 200, column 17 with a squiggly line or something + " matchadd('ALEError', '\%200l\%17v') +endfunction + +function! s:ApplyLinter(linter) + " Stop previous jobs for the same linter. + call s:ClearJob(a:linter.job) + + let a:linter.job = jobstart(a:linter.command, { + \ 'on_stdout': 's:GatherOutput', + \ 'on_exit': 's:HandleExit', + \}) + + let s:job_linter_map[a:linter.job] = a:linter + let s:job_output_map[a:linter.job] = [] + + call jobsend(a:linter.job, join(getline(1, '$'), "\n") . "\n") + call jobclose(a:linter.job, 'stdin') +endfunction + +function! s:TimerHandler() + let filetype = &filetype + let linters = ALEGetLinters(filetype) + + " Set a variable telling us to clear the loclist later. + let b:ale_should_reset_loclist = 1 + + for linter in linters + call s:ApplyLinter(linter) + endfor +endfunction + +function! ALEAddLinter(filetype, linter) + " Check if the linter program is executable before adding it. + if !executable(a:linter.executable) + return + endif + + if !has_key(s:linters, a:filetype) + let s:linters[a:filetype] = [] + endif + + call add(s:linters[a:filetype], { + \ 'job': -1, + \ 'command': a:linter.command, + \ 'callback': a:linter.callback, + \}) +endfunction + +function! ALEGetLinters(filetype) + if !has_key(s:linters, a:filetype) + return [] + endif + + return s:linters[a:filetype] +endfunction + +function! ALELint(delay) + let filetype = &filetype + let linters = ALEGetLinters(filetype) + + if s:lint_timer != -1 + call timer_stop(s:lint_timer) + let s:lint_timer = -1 + endif + + if len(linters) == 0 + " There are no linters to lint with, so stop here. + return + endif + + if a:delay > 0 + let s:lint_timer = timer_start(a:delay, 's:TimerHandler') + else + call s:TimerHandler() + endif +endfunction + +" Load all of the linters for each filetype. +runtime ale_linters/*/*.vim + +if g:ale_lint_on_text_changed + augroup ALERunOnTextChangedGroup + autocmd! + autocmd TextChanged,TextChangedI * call ALELint(g:ale_lint_delay) + augroup END +endif + +if g:ale_lint_on_enter + augroup ALERunOnEnterGroup + autocmd! + autocmd BufEnter * call ALELint(0) + augroup END +endif