├── .gitignore ├── README.md ├── autoload ├── project_lint.vim ├── project_lint │ ├── base_linter.vim │ ├── data.vim │ ├── file_explorers.vim │ ├── file_explorers │ │ ├── defx.vim │ │ ├── nerdtree.vim │ │ └── vimfiler.vim │ ├── job.vim │ ├── linters.vim │ ├── parsers.vim │ ├── queue.vim │ └── utils.vim └── vimfiler │ └── columns │ └── project_lint.vim ├── doc └── vim-project-lint.txt ├── plugin └── project_lint.vim ├── project_linters ├── css │ └── stylelint.vim ├── go │ ├── golint.vim │ ├── govet.vim │ └── revive.vim ├── javascript │ └── eslint.vim ├── lua │ ├── luac.vim │ └── luacheck.vim ├── php │ └── php.vim ├── python │ ├── flake8.vim │ └── mypy.vim ├── ruby │ ├── rubocop.vim │ └── ruby.vim ├── rust │ └── rustc.vim ├── sass │ └── stylelint.vim ├── scss │ └── stylelint.vim ├── typescript │ ├── eslint.vim │ └── tslint.vim └── vim │ └── vint.vim ├── rplugin └── python3 │ └── defx │ └── column │ └── project_lint.py ├── run-tests.sh └── test ├── data_test.vim ├── file_explorers_test.vim ├── linter_mock.vim ├── linters_test.vim ├── project_lint_test.vim └── queue_test.vim /.gitignore: -------------------------------------------------------------------------------- 1 | vim-themis 2 | test/*.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim project lint 2 | Project level lint status right in your favorite file explorer. 3 | 4 | Tested on: 5 | * Neovim 0.3.1+ - Linux 6 | * Vim 8.0+ - Linux 7 | 8 | ## File explorers 9 | 10 | ### NERDTree 11 | ![project-lint-nerdtree](https://user-images.githubusercontent.com/1782860/48678552-217fc700-eb85-11e8-9912-a2d450f53447.png) 12 | 13 | ### defx.nvim 14 | ![project-lint-defx](https://user-images.githubusercontent.com/1782860/48678553-217fc700-eb85-11e8-805f-8a43272427a8.png) 15 | 16 | ### vimfiler.vim 17 | ![project-lint-vimfiler](https://user-images.githubusercontent.com/1782860/48678554-22185d80-eb85-11e8-92fc-97e9bb1341fa.png) 18 | 19 | ## Requirements 20 | 21 | - Vim or Neovim with "jobs" feature 22 | - One of these file explorers: 23 | 1. [NERDTree](https://github.com/scrooloose/nerdtree) 24 | 2. [Defx.nvim](https://github.com/Shougo/defx.nvim) 25 | 3. [Vimfiler.vim](https://github.com/Shougo/vimfiler.vim) 26 | 27 | Optional, but highly recommended for best performance: 28 | * [ripgrep](https://github.com/BurntSushi/ripgrep) or [ag](https://github.com/ggreer/the_silver_searcher) 29 | 30 | ## Installation 31 | Choose your favorite package manager. If you don't have one, i recommend [vim-packager](https://github.com/kristijanhusak/vim-packager) 32 | 33 | ```vimL 34 | function! PackagerInit() 35 | packadd vim-packager 36 | call packager#add('kristijanhusak/vim-packager', {'type': 'opt'}) 37 | call packager#add('kristijanhusak/vim-project-lint') 38 | 39 | "File explorers. Choose your favorite. 40 | "NERDTree 41 | call packager#add('scrooloose/nerdtree') 42 | 43 | "Defx.nvim 44 | call packager#add('Shougo/defx.nvim') 45 | 46 | "Vimfiler 47 | call packager#add('Shougo/unite.vim') 48 | call packager#add('Shougo/vimfiler.vim') 49 | endfunction 50 | command! PackagerInstall call PackagerInit() | call packager#install() 51 | 52 | "NERDTree 53 | nnoremap n :NERDTree 54 | 55 | "vimfiler.vim 56 | let g:vimfiler_explorer_columns = 'project_lint:type' 57 | nnoremap n :VimfilerExplorer 58 | 59 | "defx.nvim 60 | nnoremap n :Defx -columns=project_lint:mark:filename:type 61 | ``` 62 | 63 | ## Configuration 64 | 65 | This is the default configuration: 66 | ```vimL 67 | "Styling 68 | let g:project_lint#error_icon = '✖' 69 | let g:project_lint#warning_icon = '⚠' 70 | let g:project_lint#error_icon_color = 'guifg=#fb4934 ctermfg=167' 71 | let g:project_lint#warning_icon_color = 'ctermfg=214 guifg=#fabd2f' 72 | 73 | "Linter settings 74 | "example: 75 | " let g:project_lint#enabled_linters = {'javascript': ['eslint'], 'python': ['mypy']} 76 | " If there's no setting provided for filetype, all available linters are used. 77 | " If provided an empty array fora filetype, no linting is done for it. 78 | let g:project_lint#enabled_linters = {} 79 | 80 | "example: 81 | "let g:project_lint#linter_args = {'mypy': '--ignore-missing-imports'} 82 | let g:project_lint#linter_args = {} 83 | 84 | "Folder settings 85 | "Lint status is cached for each project in this folder. 86 | let g:project_lint#cache_dir = '~/.cache/vim-project-lint' 87 | 88 | " When this is left empty, all folders from $HOME and above are ignored and not linted: 89 | " example of empty value: `['/home/kristijan', '/home', '/']` 90 | " To allow linting these folders (not recommended), set this value to `v:false` 91 | " Or use your own list of folders. When non-empty value is provided, above defaults are not added. 92 | let g:project_lint#ignored_folders = [] 93 | 94 | "Other 95 | " Echo linting progress in command line. Another way to get the progress info is to use statusline. 96 | " example: 97 | " set statusline+=project_lint#statusline() 98 | let g:project_lint#echo_progress = v:true 99 | 100 | " Prints all calls to linter commands and their responses. Mostly useful for development. 101 | let g:project_lint#debug = v:false 102 | ``` 103 | 104 | ## Available linters 105 | 106 | | Language | Linters | 107 | | -------- | ------- | 108 | | Python | [mypy](https://github.com/python/mypy), [flake8](https://github.com/PyCQA/flake8) | 109 | | Javascript | [eslint](https://github.com/eslint/eslint) | 110 | | Go | golint, go vet, [revive](https://github.com/mgechev/revive) | 111 | | Css, Sass, Scss | [stylelint](https://github.com/mgechev/revive) | 112 | | Lua | luac, luacheck | 113 | | php | php -l | 114 | | Ruby | ruby, [rubocop](https://github.com/rubocop-hq/rubocop) | 115 | | Rust | rustc | 116 | | Vim | [vint](https://github.com/Kuniwak/vint) | 117 | | Typescript | [eslint](https://github.com/eslint/eslint), [tslint](https://github.com/palantir/tslint) | 118 | 119 | ## LICENSE 120 | MIT 121 | -------------------------------------------------------------------------------- /autoload/project_lint.vim: -------------------------------------------------------------------------------- 1 | let s:lint = {} 2 | 3 | function! project_lint#new(linters, data, queue, file_explorers) abort 4 | return s:lint.new(a:linters, a:data, a:queue, a:file_explorers) 5 | endfunction 6 | 7 | function! s:lint.new(linters, data, queue, file_explorers) abort 8 | let l:instance = copy(self) 9 | let l:instance.linters = a:linters 10 | let l:instance.data = a:data 11 | let l:instance.queue = a:queue 12 | let l:instance.file_explorers = a:file_explorers 13 | let l:instance.running = v:false 14 | let l:instance.queue.on_single_job_finish = function(l:instance.single_job_finished, [], l:instance) 15 | let l:instance.initialized = v:false 16 | return l:instance 17 | endfunction 18 | 19 | function! s:lint.init() abort 20 | if !project_lint#utils#check_jobs_support() 21 | return project_lint#utils#error('Vim 8.* with "jobs" feature required.') 22 | endif 23 | 24 | if !self.file_explorers.has_valid_file_explorer() 25 | return project_lint#utils#error('No file explorer found. Install NERDTree, defx.nvim or vimfiler.') 26 | endif 27 | 28 | if has('timers') 29 | return timer_start(g:project_lint#init_delay, self.setup) 30 | endif 31 | 32 | return self.setup() 33 | endfunction 34 | 35 | function! s:lint.setup(...) abort 36 | call self.linters.load() 37 | call self.file_explorers.register() 38 | let self.initialized = v:true 39 | return self.run() 40 | endfunction 41 | 42 | function! s:lint.on_vim_leave() abort 43 | return self.queue.handle_vim_leave() 44 | endfunction 45 | 46 | function! s:lint.handle_dir_change(event) abort 47 | if a:event.scope !=? 'global' 48 | return a:event 49 | endif 50 | 51 | let l:new_root = project_lint#utils#get_project_root() 52 | if l:new_root ==? g:project_lint#root 53 | return a:event 54 | endif 55 | 56 | let g:project_lint#root = l:new_root 57 | return self.init() 58 | endfunction 59 | 60 | function! s:lint.run() abort 61 | if !self.initialized || self.is_project_ignored() 62 | return 63 | endif 64 | 65 | if self.running 66 | return project_lint#utils#error('Project lint already running.') 67 | endif 68 | 69 | let l:has_cache = self.data.check_cache() 70 | 71 | if l:has_cache 72 | call self.file_explorers.trigger_callbacks() 73 | endif 74 | 75 | for l:linter in self.linters.get() 76 | if l:linter.check_executable() && l:linter.detect() 77 | call self.set_running(l:linter, '') 78 | call self.queue.add(l:linter) 79 | endif 80 | endfor 81 | return project_lint#utils#update_statusline() 82 | endfunction 83 | 84 | function! s:lint.set_running(linter, file) abort 85 | let self.running = v:true 86 | let l:cmd = !empty(a:file) ? a:linter.file_command(a:file) : a:linter.command() 87 | 88 | call project_lint#utils#debug(printf( 89 | \ 'Linter [%s] running command for [%s]: "%s"', 90 | \ a:linter.name, 91 | \ !empty(a:file) ? a:file : 'project', 92 | \ l:cmd 93 | \ )) 94 | endfunction 95 | 96 | function! s:lint.run_file(file) abort 97 | if !self.initialized || !empty(&buftype) || self.is_project_ignored() 98 | return 99 | endif 100 | 101 | if !self.should_lint_file(a:file) 102 | return 103 | endif 104 | 105 | for l:linter in self.linters.get() 106 | if l:linter.check_executable() && l:linter.detect_for_file() 107 | if self.queue.already_linting_file(l:linter, a:file) 108 | continue 109 | endif 110 | 111 | call self.set_running(l:linter, a:file) 112 | call self.queue.add_file(l:linter, a:file) 113 | endif 114 | endfor 115 | return project_lint#utils#update_statusline() 116 | endfunction 117 | 118 | function! s:lint.should_lint_file(file) abort 119 | return stridx(a:file, g:project_lint#root) ==? 0 120 | endfunction 121 | 122 | function! s:lint.single_job_finished(linter, is_queue_empty, trigger_callbacks, ...) abort 123 | let l:type = a:0 > 0 ? a:1 : 'project' 124 | call project_lint#utils#debug(printf('Linter [%s] for [%s] finished.', a:linter.name, l:type)) 125 | call project_lint#utils#update_statusline() 126 | 127 | if a:trigger_callbacks && !a:is_queue_empty 128 | call project_lint#utils#debug(printf( 129 | \ 'Refreshing file explorer after linter [%s] finished linting [%s].' 130 | \ , a:linter.name, 131 | \ l:type)) 132 | call call(self.file_explorers.trigger_callbacks, a:000) 133 | endif 134 | 135 | if !a:is_queue_empty 136 | return 137 | endif 138 | 139 | call project_lint#utils#debug( 140 | \ 'All linters finished running. Switching to fresh data and caching it to a file.' 141 | \ ) 142 | let self.running = v:false 143 | call self.data.use_fresh_data() 144 | call call(self.file_explorers.trigger_callbacks, a:000) 145 | return self.data.cache_to_file() 146 | endfunction 147 | 148 | function! s:lint.is_project_ignored() abort 149 | return index(g:project_lint#ignored_folders, g:project_lint#root) > -1 150 | endfunction 151 | -------------------------------------------------------------------------------- /autoload/project_lint/base_linter.vim: -------------------------------------------------------------------------------- 1 | let s:base = {} 2 | 3 | function! project_lint#base_linter#get() abort 4 | return s:base 5 | endfunction 6 | 7 | function! s:base.new() abort 8 | let l:instance = copy(self) 9 | let l:instance.name = get(l:instance, 'name', '') 10 | let l:cmd_args = get(self, 'cmd_args', '') 11 | let l:user_args = get(g:project_lint#linter_args, l:instance.name, '') 12 | let l:instance.cmd_args = join(filter([l:cmd_args, l:user_args], 'v:val !=? ""'), ' ') 13 | let l:instance.stream = get(l:instance, 'stream', 'stdout') 14 | let l:instance.filetype = get(l:instance, 'filetype', []) 15 | let l:instance.format = get(l:instance, 'format', 'unix') 16 | let l:instance.enabled_filetype = g:project_lint#linters.get_enabled_filetypes(l:instance) 17 | call l:instance.check_executable() 18 | return l:instance 19 | endfunction 20 | 21 | function! s:base.detect() abort 22 | return !empty(self.cmd) && !empty(self.enabled_filetype) 23 | endfunction 24 | 25 | function! s:base.detect_for_file() abort 26 | return !empty(self.cmd) && index(self.enabled_filetype, &filetype) > -1 27 | endfunction 28 | 29 | function! s:base.check_executable() abort 30 | return self.set_cmd('') 31 | endfunction 32 | 33 | function! s:base.command() abort 34 | return printf('%s %s %s', self.cmd, self.cmd_args, g:project_lint#root) 35 | endfunction 36 | 37 | function! s:base.file_command(file) abort 38 | return printf('%s %s %s', self.cmd, self.cmd_args, a:file) 39 | endfunction 40 | 41 | function! s:base.parse(item) abort 42 | let l:path = project_lint#parsers#unix(a:item) 43 | if empty(l:path) 44 | return {} 45 | endif 46 | 47 | return self.error(l:path) 48 | endfunction 49 | 50 | function! s:base.set_cmd(cmd) abort 51 | let self.cmd = a:cmd 52 | return !empty(self.cmd) 53 | endfunction 54 | 55 | function! s:base.error(path) abort 56 | return { 'path': a:path, 'severity': 'e' } 57 | endfunction 58 | 59 | function! s:base.warning(path) abort 60 | return { 'path': a:path, 'severity': 'w' } 61 | endfunction 62 | 63 | function! s:base.parse_messages(messages) abort 64 | let l:msg = a:messages 65 | 66 | if self.format ==? 'json' && !empty(a:messages[0]) 67 | try 68 | let l:msg = json_decode(a:messages[0]) 69 | catch 70 | return [] 71 | endtry 72 | 73 | if type(l:msg) !=? type([]) 74 | return [] 75 | endif 76 | endif 77 | 78 | return l:msg 79 | endfunction 80 | -------------------------------------------------------------------------------- /autoload/project_lint/data.vim: -------------------------------------------------------------------------------- 1 | let s:data = {} 2 | 3 | function! project_lint#data#new() abort 4 | return s:data.new() 5 | endfunction 6 | 7 | function! s:data.new() abort 8 | let l:instance = copy(self) 9 | let l:instance.paths = {} 10 | let l:instance.cache = {} 11 | let l:instance.use_cache = v:false 12 | let l:instance.cache_used = v:false 13 | return l:instance 14 | endfunction 15 | 16 | function! s:data.get() abort 17 | if self.use_cache 18 | return self.cache 19 | endif 20 | 21 | return self.paths 22 | endfunction 23 | 24 | function! s:data.get_item(item) abort 25 | return get(self.get(), a:item, {}) 26 | endfunction 27 | 28 | function! s:data.check_cache() abort 29 | if self.cache_used 30 | return v:false 31 | endif 32 | 33 | let l:filename = self.cache_filename() 34 | if filereadable(l:filename) 35 | let self.cache = json_decode(readfile(l:filename)[0]) 36 | let self.use_cache = v:true 37 | return v:true 38 | endif 39 | return v:false 40 | endfunction 41 | 42 | function! s:data.add(linter, file) abort 43 | if a:file.path !~? printf('^%s', g:project_lint#root) 44 | return 45 | endif 46 | let l:should_cache_dirs = self.add_single(a:linter, a:file, v:false) 47 | let l:dir = fnamemodify(a:file.path, ':h') 48 | 49 | if l:dir ==? g:project_lint#root || !l:should_cache_dirs 50 | return 51 | endif 52 | 53 | while l:dir !=? g:project_lint#root 54 | call self.add_single(a:linter, extend(copy(a:file), {'path': l:dir }), v:true) 55 | let l:dir = fnamemodify(l:dir, ':h') 56 | endwhile 57 | endfunction 58 | 59 | function! s:data.remove(linter, file) abort 60 | if a:file !~? printf('^%s', g:project_lint#root) 61 | return 62 | endif 63 | call self.remove_single(a:linter, a:file) 64 | let l:dir = fnamemodify(a:file, ':h') 65 | 66 | if l:dir ==? g:project_lint#root 67 | return 68 | endif 69 | 70 | while l:dir !=? g:project_lint#root 71 | call self.remove_single(a:linter, l:dir) 72 | let l:dir = fnamemodify(l:dir, ':h') 73 | endwhile 74 | endfunction 75 | 76 | function! s:data.add_single(linter, file, is_dir) abort 77 | if !has_key(self.paths, a:file.path) 78 | let self.paths[a:file.path] = {} 79 | let self.paths[a:file.path][a:linter.name] = { 'w': 0, 'e': 0 } 80 | let self.paths[a:file.path][a:linter.name][a:file.severity] = 1 81 | return v:true 82 | endif 83 | 84 | if !has_key(self.paths[a:file.path], a:linter.name) 85 | let self.paths[a:file.path][a:linter.name] = { 'w': 0, 'e': 0 } 86 | let self.paths[a:file.path][a:linter.name][a:file.severity] = 1 87 | return v:true 88 | endif 89 | 90 | "Do not mark same thing as invalid more than once 91 | if !a:is_dir && self.paths[a:file.path][a:linter.name][a:file.severity] > 0 92 | return v:false 93 | endif 94 | 95 | let self.paths[a:file.path][a:linter.name][a:file.severity] += 1 96 | return v:true 97 | endfunction 98 | 99 | function! s:data.remove_single(linter, file) abort 100 | if !has_key(self.paths, a:file) || !has_key(self.paths[a:file], a:linter.name) 101 | return 102 | endif 103 | 104 | if self.paths[a:file][a:linter.name].w > 0 105 | let self.paths[a:file][a:linter.name].w -= 1 106 | endif 107 | if self.paths[a:file][a:linter.name].e > 0 108 | let self.paths[a:file][a:linter.name].e -= 1 109 | endif 110 | 111 | if self.paths[a:file][a:linter.name].w <=? 0 && self.paths[a:file][a:linter.name].e <=? 0 112 | call remove(self.paths[a:file], a:linter.name) 113 | endif 114 | 115 | if len(self.paths[a:file]) <= 2 116 | call remove(self.paths, a:file) 117 | endif 118 | endfunction 119 | 120 | function! s:data.cache_filename() abort 121 | let l:filename = tolower(substitute(g:project_lint#root, '/', '-', 'g'))[1:] 122 | let l:filename_path = printf('%s/%s.json', g:project_lint#cache_dir, l:filename) 123 | return fnamemodify(l:filename_path, ':p') 124 | endfunction 125 | 126 | function! s:data.use_fresh_data() abort 127 | let self.use_cache = v:false 128 | let self.cache_used = v:true 129 | let self.cache = {} 130 | endfunction 131 | 132 | function! s:data.add_total_severity_counters(...) abort 133 | if a:0 <=? 0 134 | for l:file in keys(self.paths) 135 | call self.add_single_severity_counter(l:file) 136 | endfor 137 | return 138 | endif 139 | 140 | let l:file = a:1 141 | let l:dir = fnamemodify(l:file, ':h') 142 | let l:is_added = self.add_single_severity_counter(l:file) 143 | 144 | if l:dir ==? g:project_lint#root || !l:is_added 145 | return 146 | endif 147 | 148 | while l:dir !=? g:project_lint#root 149 | call self.add_single_severity_counter(l:dir) 150 | let l:dir = fnamemodify(l:dir, ':h') 151 | endwhile 152 | endfunction 153 | 154 | function! s:data.add_single_severity_counter(file) abort 155 | if !has_key(self.paths, a:file) 156 | return v:false 157 | endif 158 | let l:warnings = 0 159 | let l:errors = 0 160 | for [l:linter, l:data] in items(self.paths[a:file]) 161 | if l:linter ==? 'w' || l:linter ==? 'e' 162 | continue 163 | endif 164 | 165 | if get(l:data, 'w', 0) > 0 166 | let l:warnings = 1 167 | endif 168 | 169 | if get(l:data, 'e', 0) > 0 170 | let l:errors = 1 171 | endif 172 | endfor 173 | 174 | let self.paths[a:file].w = l:warnings 175 | let self.paths[a:file].e = l:errors 176 | return v:true 177 | endfunction 178 | 179 | function! s:data.cache_to_file() abort 180 | let l:filename = self.cache_filename() 181 | 182 | if filereadable(l:filename) 183 | return writefile([json_encode(self.paths)], l:filename) 184 | endif 185 | 186 | let l:cache_dir = fnamemodify('~/.cache/vim-project-lint', ':p') 187 | if !isdirectory(l:cache_dir) 188 | call mkdir(l:cache_dir, 'p') 189 | endif 190 | 191 | return writefile([json_encode(self.paths)], l:filename) 192 | endfunction 193 | 194 | function! s:data.clear_project_cache() abort 195 | call delete(self.cache_filename()) 196 | return project_lint#utils#echo_line( 197 | \ printf('Deleted cache for project %s', g:project_lint#root) 198 | \ ) 199 | endfunction 200 | 201 | -------------------------------------------------------------------------------- /autoload/project_lint/file_explorers.vim: -------------------------------------------------------------------------------- 1 | let s:file_explorers = {} 2 | 3 | function! project_lint#file_explorers#new() abort 4 | return s:file_explorers.new() 5 | endfunction 6 | 7 | function! s:file_explorers.new() abort 8 | let l:instance = copy(self) 9 | let l:instance.explorers = [] 10 | return l:instance 11 | endfunction 12 | 13 | function! s:file_explorers.has_valid_file_explorer() abort 14 | return self.has_defx() || self.has_nerdtree() || self.has_vimfiler() 15 | endfunction 16 | 17 | function! s:file_explorers.register() abort 18 | if self.has_defx() 19 | call add(self.explorers, project_lint#file_explorers#defx#new()) 20 | endif 21 | 22 | if self.has_nerdtree() 23 | call add(self.explorers, project_lint#file_explorers#nerdtree#new()) 24 | endif 25 | 26 | if self.has_vimfiler() 27 | call add(self.explorers, project_lint#file_explorers#vimfiler#new()) 28 | endif 29 | endfunction 30 | 31 | function! s:file_explorers.trigger_callbacks(...) abort 32 | for l:explorer in self.explorers 33 | call call(l:explorer.callback, a:000) 34 | endfor 35 | endfunction 36 | 37 | function! s:file_explorers.has_defx() abort 38 | return exists('g:loaded_defx') 39 | endfunction 40 | 41 | function! s:file_explorers.has_nerdtree() abort 42 | return exists('g:loaded_nerd_tree') 43 | endfunction 44 | 45 | function! s:file_explorers.has_vimfiler() abort 46 | return exists('g:loaded_vimfiler') 47 | endfunction 48 | -------------------------------------------------------------------------------- /autoload/project_lint/file_explorers/defx.vim: -------------------------------------------------------------------------------- 1 | let s:defx = {} 2 | 3 | function! project_lint#file_explorers#defx#new() abort 4 | return s:defx.new() 5 | endfunction 6 | 7 | function! s:defx.new() abort 8 | let l:instance = copy(self) 9 | return l:instance 10 | endfunction 11 | 12 | function! s:defx.callback(...) abort 13 | if &filetype ==? 'defx' 14 | silent! exe "call defx#_do_action('redraw', [])" 15 | return 16 | endif 17 | 18 | let l:defx_winnr = bufwinnr('defx') 19 | 20 | if l:defx_winnr > 0 21 | silent! exe printf('%wincmd w', l:defx_winnr) 22 | silent! exe "call defx#_do_action('redraw', [])" 23 | silent! exe 'wincmd p' 24 | endif 25 | endfunction 26 | -------------------------------------------------------------------------------- /autoload/project_lint/file_explorers/nerdtree.vim: -------------------------------------------------------------------------------- 1 | let s:nerdtree = {} 2 | 3 | function! project_lint#file_explorers#nerdtree#new() abort 4 | return s:nerdtree.new() 5 | endfunction 6 | 7 | function! project_lint#file_explorers#nerdtree#listener(event) abort 8 | let l:subject = a:event.subject 9 | let l:path = l:subject.str() 10 | let l:data = g:project_lint#data.get_item(l:path) 11 | 12 | if !empty(l:data) 13 | if get(l:data, 'e', 0) > 0 14 | call l:subject.flagSet.removeFlag('project_lint', g:project_lint#warning_icon) 15 | return l:subject.flagSet.addFlag('project_lint', g:project_lint#error_icon) 16 | endif 17 | 18 | if get(l:data, 'w', 0) > 0 19 | call l:subject.flagSet.removeFlag('project_lint', g:project_lint#error_icon) 20 | return l:subject.flagSet.addFlag('project_lint', g:project_lint#warning_icon) 21 | endif 22 | endif 23 | 24 | return l:subject.flagSet.clearFlags('project_lint') 25 | endfunction 26 | 27 | function! s:nerdtree.new() abort 28 | let l:instance = copy(self) 29 | call l:instance.add_listeners() 30 | call l:instance.add_autocmd() 31 | return l:instance 32 | endfunction 33 | 34 | function! s:nerdtree.add_listeners() abort 35 | call g:NERDTreePathNotifier.AddListener('init', 'project_lint#file_explorers#nerdtree#listener') 36 | call g:NERDTreePathNotifier.AddListener('refresh','project_lint#file_explorers#nerdtree#listener') 37 | call g:NERDTreePathNotifier.AddListener('refreshFlags','project_lint#file_explorers#nerdtree#listener') 38 | endfunction 39 | 40 | function! s:nerdtree.add_autocmd() abort 41 | augroup project_lint_nerdtree 42 | autocmd FileType nerdtree call s:add_highlighting() 43 | augroup END 44 | 45 | call s:add_highlighting() 46 | endfunction 47 | 48 | function! s:nerdtree.callback(...) abort 49 | if !g:NERDTree.IsOpen() && !exists('b:NERDTree') 50 | return 51 | endif 52 | 53 | if a:0 > 0 54 | return self.refresh_file(a:1) 55 | endif 56 | 57 | return self.refresh_tree() 58 | endfunction 59 | 60 | function! s:nerdtree.refresh_file(file) abort 61 | if !g:NERDTree.IsOpen() 62 | return 63 | endif 64 | 65 | let l:winnr = winnr() 66 | let l:altwinnr = winnr('#') 67 | 68 | call g:NERDTree.CursorToTreeWin() 69 | let l:node = b:NERDTree.root.findNode(g:NERDTreePath.New(a:file)) 70 | if empty(l:node) 71 | return 72 | endif 73 | call l:node.refreshFlags() 74 | let l:node = l:node.parent 75 | while !empty(l:node) 76 | call l:node.refreshDirFlags() 77 | let l:node = l:node.parent 78 | endwhile 79 | 80 | call NERDTreeRender() 81 | 82 | exec l:altwinnr . 'wincmd w' 83 | exec l:winnr . 'wincmd w' 84 | endfunction 85 | 86 | function! s:nerdtree.refresh_tree() abort 87 | if exists('b:NERDTree') 88 | call b:NERDTree.root.refreshFlags() 89 | return NERDTreeRender() 90 | endif 91 | 92 | " Do not update when a special buffer is selected 93 | if !empty(&l:buftype) 94 | return 95 | endif 96 | 97 | let l:winnr = winnr() 98 | let l:altwinnr = winnr('#') 99 | 100 | call g:NERDTree.CursorToTreeWin() 101 | call b:NERDTree.root.refreshFlags() 102 | call NERDTreeRender() 103 | 104 | exec l:altwinnr . 'wincmd w' 105 | exec l:winnr . 'wincmd w' 106 | endfunction 107 | 108 | function! s:add_highlighting() abort 109 | let l:padding = '' 110 | if exists('g:loaded_nerdtree_git_status') 111 | let l:padding = '[^\(\[\|\]\)]*\zs' 112 | endif 113 | silent! exe printf('syn match NERDTreeProjectLintErrorIcon #%s%s# containedin=NERDTreeFlags', l:padding, g:project_lint#error_icon) 114 | silent! exe printf('hi default NERDTreeProjectLintErrorIcon %s', g:project_lint#error_icon_color) 115 | silent! exe printf('syn match NERDTreeProjectLintWarningIcon #%s%s# containedin=NERDTreeFlags', l:padding, g:project_lint#warning_icon) 116 | silent! exe printf('hi default NERDTreeProjectLintWarningIcon %s', g:project_lint#warning_icon_color) 117 | endfunction 118 | -------------------------------------------------------------------------------- /autoload/project_lint/file_explorers/vimfiler.vim: -------------------------------------------------------------------------------- 1 | let s:vimfiler = {} 2 | 3 | function! project_lint#file_explorers#vimfiler#new() abort 4 | return s:vimfiler.new() 5 | endfunction 6 | 7 | function! s:vimfiler.new() abort 8 | let l:instance = copy(self) 9 | return l:instance 10 | endfunction 11 | 12 | function! s:vimfiler.callback(...) abort 13 | if &filetype ==? 'vimfiler' 14 | return vimfiler#view#_redraw_screen() 15 | endif 16 | 17 | let l:vimfiler_winnr = bufwinnr('vimfiler') 18 | let l:is_vimfiler_opened = bufwinnr('vimfiler') > 0 19 | 20 | if l:vimfiler_winnr > 0 21 | silent! exe printf('%wincmd w') 22 | silent! exe 'wincmd p' 23 | endif 24 | endfunction 25 | -------------------------------------------------------------------------------- /autoload/project_lint/job.vim: -------------------------------------------------------------------------------- 1 | " Author: Prabir Shrestha 2 | " Website: https://github.com/prabirshrestha/async.vim 3 | " License: The MIT License {{{ 4 | " The MIT License (MIT) 5 | " 6 | " Copyright (c) 2016 Prabir Shrestha 7 | " 8 | " Permission is hereby granted, free of charge, to any person obtaining a copy 9 | " of this software and associated documentation files (the "Software"), to deal 10 | " in the Software without restriction, including without limitation the rights 11 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | " copies of the Software, and to permit persons to whom the Software is 13 | " furnished to do so, subject to the following conditions: 14 | " 15 | " The above copyright notice and this permission notice shall be included in all 16 | " copies or substantial portions of the Software. 17 | " 18 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | " SOFTWARE. 25 | " }}} 26 | 27 | let s:save_cpo = &cpoptions 28 | set cpoptions&vim 29 | 30 | let s:jobidseq = 0 31 | let s:jobs = {} " { job, opts, type: 'vimjob|nvimjob'} 32 | let s:job_type_nvimjob = 'nvimjob' 33 | let s:job_type_vimjob = 'vimjob' 34 | let s:job_error_unsupported_job_type = -2 " unsupported job type 35 | 36 | function! s:job_supported_types() abort 37 | let l:supported_types = [] 38 | if has('nvim') 39 | let l:supported_types += [s:job_type_nvimjob] 40 | endif 41 | if !has('nvim') && has('job') && has('channel') && has('lambda') 42 | let l:supported_types += [s:job_type_vimjob] 43 | endif 44 | return l:supported_types 45 | endfunction 46 | 47 | function! s:job_supports_type(type) abort 48 | return index(s:job_supported_types(), a:type) >= 0 49 | endfunction 50 | 51 | function! s:out_cb(jobid, opts, job, data) abort 52 | if has_key(a:opts, 'on_stdout') 53 | call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') 54 | endif 55 | endfunction 56 | 57 | function! s:err_cb(jobid, opts, job, data) abort 58 | if has_key(a:opts, 'on_stderr') 59 | call a:opts.on_stderr(a:jobid, split(a:data, "\n", 1), 'stderr') 60 | endif 61 | endfunction 62 | 63 | function! s:exit_cb(jobid, opts, job, status) abort 64 | if has_key(a:opts, 'on_exit') 65 | call a:opts.on_exit(a:jobid, a:status, 'exit') 66 | endif 67 | if has_key(s:jobs, a:jobid) 68 | call remove(s:jobs, a:jobid) 69 | endif 70 | endfunction 71 | 72 | function! s:on_stdout(jobid, data, event) abort 73 | if has_key(s:jobs, a:jobid) 74 | let l:jobinfo = s:jobs[a:jobid] 75 | if has_key(l:jobinfo.opts, 'on_stdout') 76 | call l:jobinfo.opts.on_stdout(a:jobid, a:data, a:event) 77 | endif 78 | endif 79 | endfunction 80 | 81 | function! s:on_stderr(jobid, data, event) abort 82 | if has_key(s:jobs, a:jobid) 83 | let l:jobinfo = s:jobs[a:jobid] 84 | if has_key(l:jobinfo.opts, 'on_stderr') 85 | call l:jobinfo.opts.on_stderr(a:jobid, a:data, a:event) 86 | endif 87 | endif 88 | endfunction 89 | 90 | function! s:on_exit(jobid, status, event) abort 91 | if has_key(s:jobs, a:jobid) 92 | let l:jobinfo = s:jobs[a:jobid] 93 | if has_key(l:jobinfo.opts, 'on_exit') 94 | call l:jobinfo.opts.on_exit(a:jobid, a:status, a:event) 95 | endif 96 | endif 97 | endfunction 98 | 99 | function! s:job_start(cmd, opts) abort 100 | let l:jobtypes = s:job_supported_types() 101 | let l:jobtype = '' 102 | 103 | if has_key(a:opts, 'type') 104 | if type(a:opts.type) == type('') 105 | if !s:job_supports_type(a:opts.type) 106 | return s:job_error_unsupported_job_type 107 | endif 108 | let l:jobtype = a:opts.type 109 | else 110 | let l:jobtypes = a:opts.type 111 | endif 112 | endif 113 | 114 | if empty(l:jobtype) 115 | " find the best jobtype 116 | for l:jobtype2 in l:jobtypes 117 | if s:job_supports_type(l:jobtype2) 118 | let l:jobtype = l:jobtype2 119 | endif 120 | endfor 121 | endif 122 | 123 | if l:jobtype ==? '' 124 | return s:job_error_unsupported_job_type 125 | endif 126 | 127 | if l:jobtype == s:job_type_nvimjob 128 | let l:cmd = type(a:cmd) ==? type([]) ? join(' ') : a:cmd 129 | let l:job = jobstart(l:cmd, { 130 | \ 'on_stdout': function('s:on_stdout'), 131 | \ 'on_stderr': function('s:on_stderr'), 132 | \ 'on_exit': function('s:on_exit'), 133 | \ 'stdout_buffered': v:true, 134 | \ 'cwd': has_key(a:opts, 'cwd') ? a:opts.cwd : getcwd() 135 | \}) 136 | if l:job <= 0 137 | return l:job 138 | endif 139 | let l:jobid = l:job " nvimjobid and internal jobid is same 140 | let s:jobs[l:jobid] = { 141 | \ 'type': s:job_type_nvimjob, 142 | \ 'opts': a:opts, 143 | \ } 144 | let s:jobs[l:jobid].job = l:job 145 | elseif l:jobtype == s:job_type_vimjob 146 | let s:jobidseq = s:jobidseq + 1 147 | let l:jobid = s:jobidseq 148 | 149 | let l:cmd = type(a:cmd) ==? type([]) ? join(a:cmd, ' ') : a:cmd 150 | let l:job = job_start(printf('%s %s "%s"', &shell, &shellcmdflag, l:cmd), { 151 | \ 'out_cb': function('s:out_cb', [l:jobid, a:opts]), 152 | \ 'err_cb': function('s:err_cb', [l:jobid, a:opts]), 153 | \ 'exit_cb': function('s:exit_cb', [l:jobid, a:opts]), 154 | \ 'mode': 'raw', 155 | \ 'cwd': has_key(a:opts, 'cwd') ? a:opts.cwd : getcwd() 156 | \}) 157 | if job_status(l:job) !=? 'run' 158 | return -1 159 | endif 160 | let s:jobs[l:jobid] = { 161 | \ 'type': s:job_type_vimjob, 162 | \ 'opts': a:opts, 163 | \ 'job': l:job, 164 | \ 'channel': job_getchannel(l:job), 165 | \ 'buffer': '' 166 | \ } 167 | else 168 | return s:job_error_unsupported_job_type 169 | endif 170 | 171 | return l:jobid 172 | endfunction 173 | 174 | function! s:job_stop(jobid) abort 175 | if has_key(s:jobs, a:jobid) 176 | let l:jobinfo = s:jobs[a:jobid] 177 | if l:jobinfo.type == s:job_type_nvimjob 178 | call jobstop(a:jobid) 179 | elseif l:jobinfo.type == s:job_type_vimjob 180 | call job_stop(s:jobs[a:jobid].job) 181 | endif 182 | if has_key(s:jobs, a:jobid) 183 | call remove(s:jobs, a:jobid) 184 | endif 185 | endif 186 | endfunction 187 | 188 | function! s:job_send(jobid, data) abort 189 | let l:jobinfo = s:jobs[a:jobid] 190 | if l:jobinfo.type == s:job_type_nvimjob 191 | call jobsend(a:jobid, a:data) 192 | elseif l:jobinfo.type == s:job_type_vimjob 193 | let l:jobinfo.buffer .= a:data 194 | call s:flush_vim_sendraw(a:jobid, v:null) 195 | endif 196 | endfunction 197 | 198 | function! s:flush_vim_sendraw(jobid, timer) abort 199 | " https://github.com/vim/vim/issues/2548 200 | " https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091 201 | let l:jobinfo = s:jobs[a:jobid] 202 | if len(l:jobinfo.buffer) <= 1024 203 | call ch_sendraw(l:jobinfo.channel, l:jobinfo.buffer) 204 | let l:jobinfo.buffer = '' 205 | else 206 | let l:to_send = l:jobinfo.buffer[:1023] 207 | let l:jobinfo.buffer = l:jobinfo.buffer[1024:] 208 | call ch_sendraw(l:jobinfo.channel, l:to_send) 209 | call timer_start(0, function('s:flush_vim_sendraw', [a:jobid])) 210 | endif 211 | endfunction 212 | 213 | function! s:job_wait_single(jobid, timeout, start) abort 214 | if !has_key(s:jobs, a:jobid) 215 | return -3 216 | endif 217 | 218 | let l:jobinfo = s:jobs[a:jobid] 219 | if l:jobinfo.type == s:job_type_nvimjob 220 | let l:timeout = a:timeout - reltimefloat(reltime(a:start)) * 1000 221 | return jobwait([a:jobid], float2nr(l:timeout))[0] 222 | elseif l:jobinfo.type == s:job_type_vimjob 223 | let l:timeout = a:timeout / 1000.0 224 | try 225 | while l:timeout < 0 || reltimefloat(reltime(a:start)) < l:timeout 226 | let l:info = job_info(l:jobinfo.job) 227 | if l:info.status ==# 'dead' 228 | return l:info.exitval 229 | elseif l:info.status ==# 'fail' 230 | return -3 231 | endif 232 | sleep 1m 233 | endwhile 234 | catch /^Vim:Interrupt$/ 235 | return -2 236 | endtry 237 | endif 238 | return -1 239 | endfunction 240 | 241 | function! s:job_wait(jobids, timeout) abort 242 | let l:start = reltime() 243 | let l:exitcode = 0 244 | let l:ret = [] 245 | for l:jobid in a:jobids 246 | if l:exitcode != -2 " Not interrupted. 247 | let l:exitcode = s:job_wait_single(l:jobid, a:timeout, l:start) 248 | endif 249 | let l:ret += [l:exitcode] 250 | endfor 251 | return l:ret 252 | endfunction 253 | 254 | " public apis {{{ 255 | let s:job = {} 256 | 257 | function! project_lint#job#new() abort 258 | return s:job 259 | endfunction 260 | 261 | function! s:job.start(cmd, opts) abort 262 | return s:job_start(a:cmd, a:opts) 263 | endfunction 264 | 265 | function! s:job.stop(jobid) abort 266 | call s:job_stop(a:jobid) 267 | endfunction 268 | 269 | function! s:job.send(jobid, data) abort 270 | call s:job_send(a:jobid, a:data) 271 | endfunction 272 | 273 | function! s:job.wait(jobids, ...) abort 274 | let l:timeout = get(a:000, 0, -1) 275 | return s:job_wait(a:jobids, l:timeout) 276 | endfunction 277 | " }}} 278 | -------------------------------------------------------------------------------- /autoload/project_lint/linters.vim: -------------------------------------------------------------------------------- 1 | let s:linters = {} 2 | let s:filetypes = { 3 | \ 'javascript': ['project_lint#utils#has_file_in_cwd', 'package.json'], 4 | \ 'typescript': ['project_lint#utils#find_extension', 'ts'], 5 | \ 'python': ['project_lint#utils#find_extension', 'py'], 6 | \ 'go': ['project_lint#utils#find_extension', 'go'], 7 | \ 'vim': ['project_lint#utils#find_extension', 'vim'], 8 | \ 'css': ['project_lint#utils#find_extension', 'css'], 9 | \ 'scss': ['project_lint#utils#find_extension', 'scss'], 10 | \ 'sass': ['project_lint#utils#find_extension', 'sass'], 11 | \ 'ruby': ['project_lint#utils#find_extension', 'rb'], 12 | \ 'php': ['project_lint#utils#find_extension', 'php'], 13 | \ 'lua': ['project_lint#utils#find_extension', 'lua'], 14 | \ 'rust': ['project_lint#utils#find_extension', 'rs'], 15 | \ } 16 | 17 | function! project_lint#linters#new() abort 18 | return s:linters.new() 19 | endfunction 20 | 21 | function! s:linters.new() abort 22 | let l:instance = copy(self) 23 | let l:instance.items = {} 24 | return l:instance 25 | endfunction 26 | 27 | function! s:linters.load() abort 28 | for [l:filetype, l:func] in items(s:filetypes) 29 | let l:detected = call(l:func[0], [l:func[1]]) 30 | if !empty(l:detected) 31 | silent! exe printf('runtime! project_linters/%s/*', l:filetype) 32 | endif 33 | endfor 34 | endfunction 35 | 36 | function! s:linters.get() abort 37 | return values(self.items) 38 | endfunction 39 | 40 | function! s:linters.get_linter(name) abort 41 | return get(self.items, a:name, {}) 42 | endfunction 43 | 44 | function! s:linters.add(linter) abort 45 | if !has_key(self.items, a:linter.name) 46 | let self.items[a:linter.name] = a:linter 47 | endif 48 | endfunction 49 | 50 | function! s:linters.get_enabled_filetypes(linter) abort 51 | let l:filetypes = copy(a:linter.filetype) 52 | for l:ft in l:filetypes 53 | if !has_key(g:project_lint#enabled_linters, l:ft) 54 | continue 55 | endif 56 | let l:data = g:project_lint#enabled_linters[l:ft] 57 | if index(l:data, a:linter.name) < 0 58 | call remove(l:filetypes, index(l:filetypes, l:ft)) 59 | endif 60 | endfor 61 | 62 | return l:filetypes 63 | endfunction 64 | 65 | function! s:linters.is_linter_enabled(linter) abort 66 | return !empty(self.get_enabled_filetypes(a:linter)) 67 | endfunction 68 | 69 | function! s:linters.is_linter_enabled_for_filetype(linter, filetype) abort 70 | if !has_key(g:project_lint#enabled_linters, a:filetype) 71 | return v:true 72 | endif 73 | return index(g:project_lint#enabled_linters[a:filetype], a:linter.name) > -1 74 | endfunction 75 | -------------------------------------------------------------------------------- /autoload/project_lint/parsers.vim: -------------------------------------------------------------------------------- 1 | function! project_lint#parsers#unix(item) abort 2 | let l:pattern = '^\(\/\?[^:]*\):\d*.*$' 3 | if empty(a:item) || a:item !~? l:pattern 4 | return '' 5 | endif 6 | 7 | let l:list = matchlist(a:item, l:pattern) 8 | 9 | if len(l:list) < 2 || empty(l:list[1]) 10 | return '' 11 | endif 12 | 13 | let l:item = l:list[1] 14 | 15 | if stridx(l:item, g:project_lint#root) > -1 16 | return l:item 17 | endif 18 | 19 | return printf('%s/%s', g:project_lint#root, l:item) 20 | endfunction 21 | 22 | function! project_lint#parsers#unix_with_severity(item, severity_pattern, severity_text, ...) abort 23 | let l:check_error_severity_first = a:0 > 0 24 | let l:file = project_lint#parsers#unix(a:item) 25 | if empty(l:file) 26 | return {} 27 | endif 28 | 29 | let l:matches = matchlist(a:item, a:severity_pattern) 30 | let l:severity = get(l:matches, 1, '') 31 | 32 | let l:severity_text = 'w' 33 | let l:fallback_severity_text = 'e' 34 | 35 | if l:check_error_severity_first 36 | let l:severity_text = 'e' 37 | let l:fallback_severity_text = 'w' 38 | endif 39 | 40 | if l:severity ==? a:severity_text 41 | return {'path': l:file, 'severity': l:severity_text } 42 | endif 43 | 44 | return {'path': l:file, 'severity': l:fallback_severity_text } 45 | endfunction 46 | -------------------------------------------------------------------------------- /autoload/project_lint/queue.vim: -------------------------------------------------------------------------------- 1 | let s:queue = {} 2 | 3 | function! project_lint#queue#new(job, data, linters) abort 4 | return s:queue.new(a:job, a:data, a:linters) 5 | endfunction 6 | 7 | function! s:queue.new(job, data, linters) abort 8 | let l:instance = copy(self) 9 | let l:instance.job = a:job 10 | let l:instance.data = a:data 11 | let l:instance.linters = a:linters 12 | let l:instance.list = {} 13 | let l:instance.post_project_lint_file_list = [] 14 | let l:instance.files = {} 15 | let l:instance.on_single_job_finish = '' 16 | return l:instance 17 | endfunction 18 | 19 | function! s:queue.add(linter) abort 20 | let l:id = self.run_job(a:linter.command(), a:linter, 'on_stdout') 21 | let self.list[l:id] = { 'linter': a:linter, 'file': '' } 22 | endfunction 23 | 24 | function! s:queue.handle_vim_leave() abort 25 | if self.is_empty() 26 | return 27 | endif 28 | 29 | let self.vim_leaved = v:true 30 | endfunction 31 | 32 | function! s:queue.add_file(linter, file) abort 33 | let l:data = { 'linter': a:linter, 'file': a:file } 34 | if self.is_linting_project() 35 | call add(self.post_project_lint_file_list, l:data) 36 | else 37 | let l:id = self.run_job(a:linter.file_command(a:file), a:linter, 'on_file_stdout', a:file) 38 | let self.list[l:id] = l:data 39 | endif 40 | let self.files[a:file] = get(self.files, a:file, {}) 41 | let self.files[a:file][a:linter.name] = {} 42 | endfunction 43 | 44 | function! s:queue.project_lint_finished(id, linter) abort 45 | let l:linter = self.list[a:id].linter 46 | call remove(self.list, a:id) 47 | let l:is_queue_empty = self.is_empty() 48 | let l:trigger_callbacks = v:true 49 | 50 | if l:is_queue_empty 51 | call self.process_post_project_lint_file_list() 52 | else 53 | for l:running_linter_name in self.get_running_linters().project 54 | let l:running_linter = self.linters.get_linter(l:running_linter_name) 55 | for l:filetype in l:running_linter.filetype 56 | if index(l:linter.filetype, l:filetype) > -1 57 | let l:trigger_callbacks = v:false 58 | break 59 | endif 60 | endfor 61 | if !l:trigger_callbacks 62 | break 63 | endif 64 | endfor 65 | endif 66 | 67 | if l:trigger_callbacks 68 | call self.data.add_total_severity_counters() 69 | endif 70 | 71 | return call(self.on_single_job_finish, [a:linter, l:is_queue_empty, l:trigger_callbacks]) 72 | endfunction 73 | 74 | function! s:queue.process_post_project_lint_file_list() abort 75 | if len(self.post_project_lint_file_list) <=? 0 76 | return 77 | endif 78 | 79 | for l:post_lint_job in self.post_project_lint_file_list 80 | call self.add_file(l:post_lint_job.linter, l:post_lint_job.file) 81 | call remove(self.post_project_lint_file_list, 0) 82 | endfor 83 | endfunction 84 | 85 | function! s:queue.file_lint_finished(id, file, linter) abort 86 | call remove(self.list, a:id) 87 | let l:old_file_state = copy(get(self.data.get(), a:file, {})) 88 | call self.data.remove(a:linter, a:file) 89 | if !empty(self.files[a:file][a:linter.name]) 90 | call self.data.add(a:linter, self.files[a:file][a:linter.name]) 91 | endif 92 | 93 | let l:is_queue_empty = self.is_empty() 94 | let l:trigger_callbacks = v:false 95 | 96 | if !l:is_queue_empty 97 | return call(self.on_single_job_finish, [a:linter, l:is_queue_empty, l:trigger_callbacks, a:file]) 98 | endif 99 | 100 | call self.data.add_total_severity_counters(a:file) 101 | let l:new_file_state = copy(get(self.data.get(), a:file, {})) 102 | 103 | if empty(l:new_file_state) !=? empty(l:old_file_state) 104 | let l:trigger_callbacks = v:true 105 | else 106 | let l:old_warning = get(l:old_file_state, 'w', 0) 107 | let l:old_error = get(l:old_file_state, 'e', 0) 108 | let l:new_warning = get(l:new_file_state, 'w', 0) 109 | let l:new_error = get(l:new_file_state, 'e', 0) 110 | 111 | let l:trigger_callbacks = l:old_warning !=? l:new_warning || l:old_error !=? l:new_error 112 | endif 113 | 114 | call remove(self.files, a:file) 115 | return call(self.on_single_job_finish, [a:linter, l:is_queue_empty, l:trigger_callbacks, a:file]) 116 | endfunction 117 | 118 | function! s:queue.is_empty() abort 119 | return len(self.list) ==? 0 120 | endfunction 121 | 122 | function! s:queue.is_linting_project() abort 123 | if self.is_empty() 124 | return v:false 125 | endif 126 | 127 | for [l:id, l:job] in items(self.list) 128 | if empty(l:job.file) 129 | return v:true 130 | endif 131 | endfor 132 | 133 | return v:false 134 | endfunction 135 | 136 | function! s:queue.already_linting_file(linter, file) abort 137 | if self.is_empty() 138 | return v:false 139 | endif 140 | 141 | for [l:id, l:job] in items(self.list) 142 | if l:job.linter.name ==? a:linter.name && l:job.file ==? a:file 143 | return v:true 144 | endif 145 | endfor 146 | 147 | for l:post_lint_job in self.post_project_lint_file_list 148 | if l:post_lint_job.linter.name ==? a:linter.name && l:post_lint_job.file ==? a:file 149 | return v:true 150 | endif 151 | endfor 152 | 153 | return v:false 154 | endfunction 155 | 156 | function! s:queue.on_stdout(linter, id, message, event) abort 157 | call project_lint#utils#debug(printf('Linter [%s] output for [project]: %s: %s', a:linter.name, a:event, string(a:message))) 158 | if a:event ==? 'exit' 159 | if has_key(self, 'vim_leaved') 160 | return 161 | endif 162 | return self.project_lint_finished(a:id, a:linter) 163 | endif 164 | 165 | if a:event !=? a:linter.stream 166 | return 167 | endif 168 | 169 | let l:messages = a:linter.parse_messages(a:message) 170 | 171 | for l:msg in l:messages 172 | let l:item = a:linter.parse(l:msg) 173 | if empty(l:item) 174 | continue 175 | endif 176 | 177 | call self.data.add(a:linter, l:item) 178 | endfor 179 | endfunction 180 | 181 | function! s:queue.on_file_stdout(linter, file, id, message, event) abort dict 182 | call project_lint#utils#debug(printf('Linter [%s] output for [%s]: %s: %s', a:linter.name, a:file, a:event, string(a:message))) 183 | if a:event ==? 'exit' 184 | if has_key(self, 'vim_leaved') 185 | return 186 | endif 187 | return self.file_lint_finished(a:id, a:file, a:linter) 188 | endif 189 | 190 | if a:event !=? a:linter.stream 191 | return 192 | endif 193 | 194 | let l:messages = a:linter.parse_messages(a:message) 195 | for l:msg in l:messages 196 | let l:item = a:linter.parse(l:msg) 197 | if empty(l:item) 198 | continue 199 | endif 200 | 201 | if l:item.path ==? a:file 202 | let self.files[a:file][a:linter.name] = l:item 203 | endif 204 | endfor 205 | endfunction 206 | 207 | function! s:queue.get_running_linters() abort 208 | let l:result = { 'project': [], 'files': [] } 209 | if self.is_empty() 210 | return l:result 211 | endif 212 | 213 | for [l:id, l:job] in items(self.list) 214 | if has_key(l:job, 'file') && !empty(l:job.file) 215 | call add(l:result.files, l:job.linter.name) 216 | else 217 | call add(l:result.project, l:job.linter.name) 218 | endif 219 | endfor 220 | 221 | return l:result 222 | endfunction 223 | 224 | function! s:queue.run_job(cmd, linter, callback, ...) abort 225 | return self.job.start(a:cmd, { 226 | \ 'on_stdout': function(self[a:callback], [a:linter] + a:000, self), 227 | \ 'on_stderr': function(self[a:callback], [a:linter] + a:000, self), 228 | \ 'on_exit': function(self[a:callback], [a:linter] + a:000, self), 229 | \ 'cwd': g:project_lint#root 230 | \ }) 231 | endfunction 232 | 233 | -------------------------------------------------------------------------------- /autoload/project_lint/utils.vim: -------------------------------------------------------------------------------- 1 | let s:statusline = '' 2 | 3 | function! project_lint#utils#update_statusline() abort 4 | let l:running_linters = g:project_lint#queue.get_running_linters() 5 | if empty(l:running_linters.project) && empty(l:running_linters.files) 6 | if g:project_lint#echo_progress && !empty(s:statusline) 7 | call project_lint#utils#echo_line('') 8 | endif 9 | let s:statusline = '' 10 | return v:false 11 | endif 12 | 13 | let l:text = [] 14 | 15 | if len(l:running_linters.project) > 0 16 | call add(l:text, printf('project with: %s', l:running_linters.project)) 17 | endif 18 | 19 | if len(l:running_linters.files) > 0 20 | call add(l:text, printf('file(s) with: %s', l:running_linters.files)) 21 | endif 22 | 23 | let l:text = join(l:text, ', ') 24 | 25 | let l:has_cache = !g:project_lint#data.cache_used && g:project_lint#data.use_cache 26 | let l:cache_text = l:has_cache ? 'Loaded from cache. Refreshing': 'Linting' 27 | let s:statusline = printf('%s %s', l:cache_text, l:text) 28 | if g:project_lint#echo_progress 29 | call project_lint#utils#echo_line(s:statusline) 30 | endif 31 | endfunction 32 | 33 | function! project_lint#utils#get_statusline() abort 34 | return s:statusline 35 | endfunction 36 | 37 | function! project_lint#utils#echo_line(text) abort 38 | if empty(a:text) 39 | echom '' 40 | return 41 | endif 42 | let l:text = a:text 43 | if type(l:text) !=? type('') 44 | let l:text = string(l:text) 45 | endif 46 | silent! exe 'redraw' 47 | echom printf('[project-lint]: %s', l:text) 48 | endfunction 49 | 50 | function! project_lint#utils#error(text) abort 51 | echohl Error 52 | call project_lint#utils#echo_line(a:text) 53 | echohl NONE 54 | endfunction 55 | 56 | let s:extensions_found = {} 57 | function! project_lint#utils#find_extension(extension) abort 58 | if has_key(s:extensions_found, a:extension) 59 | return s:extensions_found[a:extension] 60 | endif 61 | let l:items = s:find_extension(a:extension) 62 | if len(l:items) > 0 63 | let s:extensions_found[a:extension] = l:items[0] 64 | return l:items[0] 65 | endif 66 | 67 | return '' 68 | endfunction 69 | 70 | function! project_lint#utils#has_file_in_cwd(file) abort 71 | return filereadable(printf('%s/%s', g:project_lint#root, a:file)) 72 | endfunction 73 | 74 | function! project_lint#utils#debug(msg) abort 75 | if !get(g:, 'project_lint#debug', v:false) 76 | return 77 | endif 78 | 79 | return project_lint#utils#echo_line(a:msg) 80 | endfunction 81 | 82 | function! project_lint#utils#find_extension_cmd(extension) abort 83 | if executable('rg') 84 | return printf("rg --files -g '*.%s' %s", a:extension, g:project_lint#root) 85 | endif 86 | 87 | if executable('ag') 88 | return printf('ag -g "^.*\.%s$" %s', a:extension, g:project_lint#root) 89 | endif 90 | 91 | if executable('find') 92 | return printf('find %s -name "*.%s"', g:project_lint#root, a:extension) 93 | endif 94 | 95 | return '' 96 | endfunction 97 | 98 | function! s:find_extension(extension) abort 99 | let l:cmd = project_lint#utils#find_extension_cmd(a:extension) 100 | if !empty(l:cmd) 101 | return project_lint#utils#system(l:cmd) 102 | endif 103 | 104 | return glob(printf('%s/**/*.%s', g:project_lint#root, a:extension), v:false, v:true) 105 | endfunction 106 | 107 | function! project_lint#utils#get_project_root() abort 108 | let l:project_file = findfile('.vimprojectlint', printf('%s;', getcwd())) 109 | if !empty(l:project_file) 110 | let l:project_file = fnamemodify(l:project_file, ':p:h') 111 | endif 112 | let l:git_root = '' 113 | if executable('git') 114 | let l:cmd = systemlist('git rev-parse --show-toplevel') 115 | if !v:shell_error 116 | let l:git_root = fnamemodify(l:cmd[0], ':p:h') 117 | endif 118 | endif 119 | 120 | if empty(l:project_file) && empty(l:git_root) 121 | return getcwd() 122 | endif 123 | 124 | if len(l:project_file) > len(l:git_root) 125 | return l:project_file 126 | endif 127 | 128 | return l:git_root 129 | endfunction 130 | 131 | function! project_lint#utils#system(cmd) abort 132 | let l:save_shell = s:set_shell() 133 | let l:cmd_output = systemlist(a:cmd) 134 | call s:restore_shell(l:save_shell) 135 | return l:cmd_output 136 | endfunction 137 | 138 | function! s:set_shell() abort 139 | let l:save_shell = [&shell, &shellcmdflag, &shellredir] 140 | 141 | if has('win32') 142 | set shell=cmd.exe shellcmdflag=/c shellredir=>%s\ 2>&1 143 | else 144 | set shell=sh shellredir=>%s\ 2>&1 145 | endif 146 | 147 | return l:save_shell 148 | endfunction 149 | 150 | function! s:restore_shell(saved_shell) abort 151 | let [&shell, &shellcmdflag, &shellredir] = a:saved_shell 152 | endfunction 153 | 154 | function! project_lint#utils#xargs_lint_command(extension, cmd, cmd_args) abort 155 | let l:ext_cmd = project_lint#utils#find_extension_cmd(a:extension) 156 | return printf('%s | xargs -L 1 %s %s', l:ext_cmd, a:cmd, a:cmd_args) 157 | endfunction 158 | 159 | function! project_lint#utils#get_nested_key(dict, key, ...) abort 160 | let l:items = split(a:key, '\.') 161 | let l:result = get(a:dict, l:items[0], {}) 162 | let l:default = a:0 > 0 ? a:1 : {} 163 | if empty(l:result) 164 | return l:default 165 | endif 166 | 167 | for l:item in l:items[1:] 168 | if type(l:result) !=? type({}) 169 | return l:default 170 | endif 171 | 172 | let l:result = get(l:result, l:item, {}) 173 | if empty(l:result) 174 | return l:default 175 | endif 176 | endfor 177 | 178 | return l:result 179 | endfunction 180 | 181 | function! project_lint#utils#check_jobs_support() abort 182 | return has('nvim') || has('job') 183 | endfunction 184 | 185 | function! project_lint#utils#get_directories_up_to(from, to) abort 186 | let l:start = fnamemodify(a:from, ':p:h') 187 | let l:end = fnamemodify(a:to, ':p:h') 188 | let l:result = [] 189 | while l:start !=? l:end 190 | call add(l:result, l:start) 191 | let l:start = fnamemodify(l:start, ':h') 192 | endwhile 193 | 194 | call add(l:result, l:end) 195 | return l:result 196 | endfunction 197 | -------------------------------------------------------------------------------- /autoload/vimfiler/columns/project_lint.vim: -------------------------------------------------------------------------------- 1 | "============================================================================= 2 | " FILE: type.vim 3 | " AUTHOR: Shougo Matsushita 4 | " License: MIT license 5 | "============================================================================= 6 | 7 | function! vimfiler#columns#project_lint#define() abort 8 | return s:column 9 | endfunction 10 | 11 | let s:column = { 12 | \ 'name' : 'project_lint', 13 | \ 'description' : 'Get lint status of a file', 14 | \ 'syntax' : 'vimfilerColumn__ProjectLint', 15 | \ } 16 | 17 | function! s:column.length(files, context) abort 18 | return 3 19 | endfunction 20 | 21 | function! s:column.define_syntax(context) abort 22 | silent! exe printf("syntax match vimfilerColumn__ProjectLintError '\[%s\]' containedin=vimfilerColumn__ProjectLint", g:project_lint#error_icon) 23 | silent! exe printf("syntax match vimfilerColumn__ProjectLintWarning '\[%s\]' containedin=vimfilerColumn__ProjectLint", g:project_lint#warning_icon) 24 | silent! exe printf('hi default vimfilerColumn__ProjectLintError %s', g:project_lint#error_icon_color) 25 | silent! exe printf('hi default vimfilerColumn__ProjectLintWarning %s', g:project_lint#warning_icon_color) 26 | endfunction 27 | 28 | function! s:column.get(file, context) abort 29 | let l:default = ' ' 30 | let l:data = get(g:project_lint#get_data(), a:file.action__path, {}) 31 | if empty(l:data) 32 | return l:default 33 | endif 34 | 35 | if get(l:data, 'e', 0) > 0 36 | return '['.g:project_lint#error_icon.']' 37 | endif 38 | 39 | if get(l:data, 'w', 0) > 0 40 | return '['.g:project_lint#warning_icon.']' 41 | endif 42 | 43 | return l:default 44 | endfunction 45 | -------------------------------------------------------------------------------- /doc/vim-project-lint.txt: -------------------------------------------------------------------------------- 1 | *vim-project-lint.txt* *project-lint.txt* Project lint status right in your file explorer. 2 | 3 | Project lint status right in your file explorer. 4 | 5 | Author: Kristijan 6 | License: MIT 7 | 8 | vim-project-lint *vim-project-lint* 9 | 10 | 1. Introduction |vim-project-lint-introduction| 11 | 2. Install |vim-project-lint-install| 12 | 3. How it works |vim-project-lint-how-it-works| 13 | 4. Settings |vim-project-lint-settings| 14 | 5. Functions |vim-project-lint-functions| 15 | 6. Commands |vim-project-lint-commands| 16 | 7. Available linters |vim-project-lint-linters| 17 | 7.1 Javascript |vim-project-lint-linter-javascript| 18 | 7.1.1 eslint |vim-project-lint-linter-eslint| 19 | 7.2 python |vim-project-lint-linter-python| 20 | 7.2.1 mypy |vim-project-lint-linter-mypy| 21 | 7.2.2 flake8 |vim-project-lint-linter-flake8| 22 | 7.3 go |vim-project-lint-linter-go| 23 | 7.3.1 go vet |vim-project-lint-linter-govet| 24 | 7.4 vimscript |vim-project-lint-linter-vimscript| 25 | 7.4.1 vint |vim-project-lint-linter-vint| 26 | 8. Custom Linters |vim-project-lint-custom-linters| 27 | 28 | ============================================================================== 29 | 1. Introduction *vim-project-lint-introduction* 30 | 31 | Vim project lint is plugin that shows lint status for your project in your 32 | favorite file explorer. It supports: 33 | 1. NERDTree - https://github.com/scrooloose/nerdtree 34 | 2. Defx.nvim - https://github.com/Shougo/defx.nvim 35 | 2. Vimfiler.vim - https://github.com/Shougo/vimfiler.vim 36 | 37 | Main features: 38 | 39 | 1. Detects all available file types in your project 40 | 2. Uses available linters for each file type to do the linting 41 | 3. After all linters are ran, presents the state in the file explorer using flags 42 | 4. Updates the status for each file after it is written to disc (BufWritePost) 43 | 44 | ============================================================================== 45 | 2. Install *vim-project-lint-install* 46 | 47 | Requirements: 48 | - Vim or Neovim with "jobs" feature 49 | - One of these file explorers: 50 | 1. NERDTree - https://github.com/scrooloose/nerdtree 51 | 2. Defx.nvim - https://github.com/Shougo/defx.nvim 52 | 2. Vimfiler.vim - https://github.com/Shougo/vimfiler.vim 53 | 54 | Optional, but highly recommended for best performance: 55 | * ripgrep https://github.com/BurntSushi/ripgrep 56 | or 57 | * ag https://github.com/ggreer/the_silver_searcher 58 | 59 | Install with `vim-packager` https://github.com/Kristijanhusak/vim-packager: 60 | > 61 | function! PackagerInit() 62 | packadd vim-packager 63 | call packager#add('kristijanhusak/vim-packager', {'type': 'opt'}) 64 | call packager#add('kristijanhusak/vim-project-lint') 65 | "File explorers. Choose your favorite 66 | call packager#add('scrooloose/nerdtree') 67 | "or... 68 | call packager#add('Shougo/defx.nvim') 69 | "or... 70 | call packager#add('Shougo/unite.vim') 71 | call packager#add('Shougo/vimfiler.vim') 72 | " ... Other plugins 73 | endfunction 74 | 75 | command! PackagerInstall call PackagerInit() | call packager#install() 76 | < 77 | 78 | Or if you prefer `Plug` https://github.com/junegunn/vim-plug 79 | > 80 | Plug 'kristijanhusak/vim-project-lint' 81 | "File explorers. Choose your favorite 82 | Plug 'scrooloose/nerdtree' 83 | "or... 84 | Plug 'Shougo/defx.nvim' 85 | "or... 86 | Plug 'Shougo/unite.vim' 87 | Plug 'Shougo/vimfiler.vim' 88 | 89 | ============================================================================== 90 | 3. How it works *vim-project-lint-how-it-works* 91 | 92 | After Vim starts, several steps are done: 93 | 1. Project root is detected. 94 | 2. All filetypes that currently have linters are detected. 95 | 3. Runs all linters in parallel and updates the file explorer with the status 96 | 97 | Short explanation of each step: 98 | 1. Project root is detected. 99 | Project root is detected by several things. Here's the list by priority: 100 | 1. Folder that has `.vimprojectlint` file 101 | 2. Git repository folder (`git rev-parse --show-toplevel`) 102 | 3. Current working directory (`getcwd()`) 103 | 104 | 2. Filetypes detected. 105 | Using external tools or `glob()`, files with specific extensions are found. 106 | It's recommended to install [ripgrep](https://github.com/BurntSushi/ripgrep) 107 | or [ag](https://github.com/ggreer/the_silver_searcher). If none of those are 108 | found, fallbacks to `find` or Vim's `glob()` 109 | 110 | 3. Runs all linters 111 | Self explanatory. Progress can be viewed in command line or statusline 112 | |project_lint#statusline| 113 | 114 | 115 | ============================================================================== 116 | 4. Settings *vim-project-lint-settings* 117 | 118 | *g:project_lint#error_icon* 119 | g:project_lint#error_icon 120 | Which icon to use to mark files with errors in file explorer. 121 | Example: `let g:project_lint#error_icon = 'x'` 122 | 123 | Default: `✖` 124 | 125 | *g:project_lint#error_icon_color* 126 | g:project_lint#error_icon_color 127 | Which color to use for the |g:project_lint#error_icon| 128 | 129 | Example of strict red: `guifg=#FF000` 130 | 131 | Default: `guifg=#fb4934 ctermfg=167` 132 | 133 | *g:project_lint#warning_icon* 134 | g:project_lint#warning_icon 135 | Which icon to use to mark files with warnings in file explorer. 136 | Example: `let g:project_lint#warning_icon = '-'` 137 | 138 | Default: `⚠` 139 | 140 | *g:project_lint#warning_icon_color* 141 | g:project_lint#warning_icon_color 142 | Which color to use for the |g:project_lint#warning_icon| 143 | 144 | Example of yellowish: `guifg=#FFFF00 145 | 146 | Default: `guifg=#fabd2f ctermfg=214` 147 | 148 | *g:project_lint#enabled_linters* 149 | g:project_lint#enabled_linters 150 | Set enabled linters per filetype. 151 | When filetype is not defined here, all available linters are 152 | used. 153 | When set to empty array, no linting is done for that 154 | filetype. 155 | 156 | Example: 157 | > 158 | let g:project_lint#enabled_linters = {'javascript': ['eslint'], 'python': ['mypy']} 159 | < 160 | 161 | Default: `{}` 162 | 163 | *g:project_lint#linter_args* 164 | g:project_lint#linter_args 165 | Dict containing additional arguments that are passed to 166 | linter commands. 167 | 168 | Example: 169 | `let g:project_lint#linter_args = {` 170 | `\ 'mypy': '--ignore-missing-iports'` 171 | `\ }` 172 | 173 | Default: `{}` 174 | 175 | *g:project_lint#cache_dir* 176 | g:project_lint#cache_dir 177 | Each project lint status is cached into a file, which is used 178 | later when vim starts, to return instant feedback until the 179 | new status is retrieved. 180 | 181 | Default: `~/.cache/vim-project-lint` 182 | 183 | *g:project_lint#ignored_folders* 184 | g:project_lint#ignored_folders 185 | Which folders to ignore. 186 | When left empty, all folders from `$HOME` and above are 187 | ignored and not linted. 188 | To completely disable this variable (not recommended), set it 189 | to `v:false` 190 | When provided as non-empty array, only those folders are 191 | ignored, which means above defaults are not applied. 192 | 193 | Default: `$HOME and up (ex: ['/home/kristijan', '/home', '/'])` 194 | 195 | *g:project_lint#echo_progress* 196 | g:project_lint#echo_progress 197 | Print linting progress in command line. 198 | 199 | Default: `v:true` 200 | 201 | *g:project_lint#debug* 202 | g:project_lint#debug 203 | Print all linter commands and their outputs in command line. 204 | Mostly useful for development and debugging. 205 | 206 | Default: `v:false` 207 | 208 | ============================================================================== 209 | 5. Functions *vim-project-lint-functions* 210 | 211 | *project_lint#statusline()* 212 | project_lint#statusline() 213 | Prints the current status of the linter process. 214 | 215 | `set statusline+=%{project_lint#statusline()}` 216 | 217 | Example output (Project linting): 218 | `Linting project with: ['eslint', 'mypy']` 219 | 220 | Example output (Single file linting): 221 | `Linting file with: ['eslint', 'mypy']` 222 | 223 | ============================================================================== 224 | 6. Commands *vim-project-lint-commands* 225 | 226 | *ProjectLintRun* 227 | ProjectLintRun 228 | Re-run linting of the project manually. Will not work if 229 | there's currently linting in progress. 230 | 231 | *ProjectLintRunFile* 232 | ProjectLintRunFile 233 | Run linting of the current file. Will not work if 234 | there's currently linting in progress. 235 | This is also automatically triggered after file is saved. 236 | 237 | *ProjectLintClearCache* 238 | ProjectLintClearCache 239 | Clear cache data of the current project. 240 | 241 | ============================================================================== 242 | 7. Available Linters *vim-project-lint-linters* 243 | 244 | Below linters are available. 245 | 246 | ============================================================================== 247 | 7.1 Javascript *vim-project-lint-linters-javascript* 248 | 249 | ============================================================================== 250 | 7.1.1 Eslint *vim-project-lint-linters-esilnt* 251 | 252 | ============================================================================== 253 | 7.2 Python *vim-project-lint-linters-python* 254 | 255 | ============================================================================== 256 | 7.2.1 mypy *vim-project-lint-linters-mypy* 257 | 258 | ============================================================================== 259 | 7.2.2 flake8 *vim-project-lint-linters-flake8* 260 | 261 | ============================================================================== 262 | 7.3 Go *vim-project-lint-linters-go* 263 | 264 | ============================================================================== 265 | 7.3.1 go vet *vim-project-lint-linters-govet* 266 | 267 | aliased as `govet` 268 | 269 | ============================================================================== 270 | 7.4 Vimscript *vim-project-lint-linters-viscript* 271 | 272 | ============================================================================== 273 | 7.4.1 vint *vim-project-lint-linters-vint* 274 | 275 | ============================================================================== 276 | 8. Custom linters *vim-project-lint-custom-linters* 277 | 278 | All currently available linters live in `project_linters` folder. 279 | All linters should extend `project_lint#base_linter#get()` , and have few 280 | custom defined methods and properties: 281 | 282 | Properties: 283 | 1. `name` - Linter name. This is used for allowing configuration per linter. 284 | 2. `filetype` - List of filetypes to apply this linter to. Must be array. 285 | 3. `cmd_args` - Arguments to pass to linter command. Most of the time it 286 | contains only desired output format. 287 | 288 | 1. `check_executable()` - Check if linter executable exists in the system and 289 | attach it to the linter instance 290 | 2. `command()` - system command to use for project level linting 291 | 3. `file_command(file)` - system command to use for file linting 292 | 4. `parse(item)` - parse linter output into dictionary readable by project 293 | lint. 294 | 295 | Other methods can be overwritten, but most of the time that's not necessary. 296 | 297 | For more information check these files: 298 | * `autoload/project_lint/base_linter.vim` 299 | * Any linter in `project_linters` folder. 300 | 301 | Note: Whole project lint plugin is written mostly in OOP style 302 | (For more info, see 303 | http://got-ravings.blogspot.com/2008/09/vim-pr0n-prototype-based-objects.html 304 | ). 305 | 306 | ============================================================================== 307 | 308 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 309 | -------------------------------------------------------------------------------- /plugin/project_lint.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf-8 2 | if exists('g:loaded_project_lint') 3 | finish 4 | endif 5 | let g:loaded_project_lint = v:true 6 | 7 | let g:project_lint#error_icon = get(g:, 'project_lint#error_icon', '✖') 8 | let g:project_lint#warning_icon = get(g:, 'project_lint#warning_icon', '⚠') 9 | let g:project_lint#error_icon_color = get(g:, 'project_lint#error_icon_color', 'guifg=#fb4934 ctermfg=167') 10 | let g:project_lint#warning_icon_color = get(g:, 'project_lint#warning_icon_color', 'ctermfg=214 guifg=#fabd2f') 11 | let g:project_lint#enabled_linters = get(g:, 'project_lint#enabled_linters', {}) 12 | let g:project_lint#linter_args = get(g:, 'project_lint#linter_args', {}) 13 | let g:project_lint#debug = get(g:, 'project_lint#debug', v:false) 14 | let g:project_lint#cache_dir = get(g:, 'project_lint#cache_dir', '~/.cache/vim-project-lint') 15 | let g:project_lint#echo_progress = get(g:, 'project_lint#echo_progress', v:true) 16 | let g:project_lint#ignored_folders = get(g:, 'project_lint#ignored_folders', []) 17 | let g:project_lint#defx_column_length = get(g:, 'project_lint#defx_column_length', 1) 18 | let g:project_lint#init_delay = get(g:, 'project_lint#init_delay', 1000) 19 | 20 | let g:project_lint#root = project_lint#utils#get_project_root() 21 | let g:project_lint#file_explorers = project_lint#file_explorers#new() 22 | let g:project_lint#job = project_lint#job#new() 23 | let g:project_lint#data = project_lint#data#new() 24 | let g:project_lint#linters = project_lint#linters#new() 25 | let g:project_lint#queue = project_lint#queue#new( 26 | \ g:project_lint#job, 27 | \ g:project_lint#data, 28 | \ g:project_lint#linters 29 | \ ) 30 | let g:project_lint = project_lint#new( 31 | \ g:project_lint#linters, 32 | \ g:project_lint#data, 33 | \ g:project_lint#queue, 34 | \ g:project_lint#file_explorers, 35 | \ ) 36 | 37 | function! project_lint#statusline() 38 | return project_lint#utils#get_statusline() 39 | endfunction 40 | 41 | function! project_lint#get_data() 42 | return g:project_lint#data.get() 43 | endfunction 44 | 45 | command! -nargs=0 ProjectLintClearCache call g:project_lint#data.clear_project_cache() 46 | command! -nargs=0 ProjectLintRun call g:project_lint.run() 47 | command! -nargs=0 ProjectLintRunFile call g:project_lint.run_file(expand('%:p')) 48 | 49 | if type(g:project_lint#ignored_folders) ==? type([]) && empty(g:project_lint#ignored_folders) 50 | let g:project_lint#ignored_folders = project_lint#utils#get_directories_up_to($HOME, '/') 51 | endif 52 | 53 | augroup project_lint 54 | autocmd! 55 | autocmd VimEnter * call g:project_lint.init() 56 | autocmd VimLeave * call g:project_lint.on_vim_leave() 57 | if has('nvim') || has('8.0.1459') 58 | autocmd DirChanged * call project_lint.handle_dir_change(v:event) 59 | endif 60 | autocmd BufWritePost * call g:project_lint.run_file(expand(':p')) 61 | augroup END 62 | -------------------------------------------------------------------------------- /project_linters/css/stylelint.vim: -------------------------------------------------------------------------------- 1 | "Note: This file has an exact copy for scss and sass filetype 2 | let s:stylelint = copy(project_lint#base_linter#get()) 3 | let s:stylelint.name = 'stylelint' 4 | let s:stylelint.stream = 'stdout' 5 | let s:stylelint.filetype = ['css', 'scss', 'sass'] 6 | let s:stylelint.cmd_args = '--no-color -f unix' 7 | 8 | function! s:stylelint.check_executable() abort 9 | let l:local = printf('%s/node_modules/.bin/stylelint', g:project_lint#root) 10 | let l:global = 'stylelint' 11 | if executable(l:local) 12 | return self.set_cmd(l:local) 13 | endif 14 | 15 | if executable(l:global) 16 | return self.set_cmd(l:global) 17 | endif 18 | 19 | return self.set_cmd('') 20 | endfunction 21 | 22 | function! s:stylelint.command() abort 23 | return printf( 24 | \ '%s %s %s/**/*.{%s}', 25 | \ self.cmd, 26 | \ self.cmd_args, 27 | \ g:project_lint#root, 28 | \ join(self.enabled_filetype, ',') 29 | \ ) 30 | endfunction 31 | 32 | function! s:stylelint.file_command(file) abort 33 | return printf('%s %s %s', self.cmd, self.cmd_args, a:file) 34 | endfunction 35 | 36 | function! s:stylelint.parse(item) abort 37 | return project_lint#parsers#unix_with_severity( 38 | \ a:item, 39 | \ '^.*\s\[\(error\|warning\)\]$', 40 | \ 'warning' 41 | \ ) 42 | endfunction 43 | 44 | let s:instance = s:stylelint.new() 45 | 46 | function! project_linters#css#stylelint#get() abort 47 | return s:instance 48 | endfunction 49 | 50 | call g:project_lint#linters.add(s:instance) 51 | -------------------------------------------------------------------------------- /project_linters/go/golint.vim: -------------------------------------------------------------------------------- 1 | let s:golint = copy(project_lint#base_linter#get()) 2 | let s:golint.name = 'golint' 3 | let s:golint.filetype = ['go'] 4 | 5 | function! s:golint.check_executable() abort 6 | if executable('golint') 7 | return self.set_cmd('golint') 8 | endif 9 | 10 | return self.set_cmd('') 11 | endfunction 12 | 13 | function! s:golint.command() abort 14 | return printf('%s %s %s/...', self.cmd, self.cmd_args, g:project_lint#root) 15 | endfunction 16 | 17 | call g:project_lint#linters.add(s:golint.new()) 18 | -------------------------------------------------------------------------------- /project_linters/go/govet.vim: -------------------------------------------------------------------------------- 1 | let s:govet = copy(project_lint#base_linter#get()) 2 | let s:govet.name = 'govet' 3 | let s:govet.stream = 'stderr' 4 | let s:govet.filetype = ['go'] 5 | 6 | function! s:govet.check_executable() abort 7 | if executable('go') 8 | return self.set_cmd('go vet') 9 | endif 10 | 11 | return self.set_cmd('') 12 | endfunction 13 | 14 | call g:project_lint#linters.add(s:govet.new()) 15 | -------------------------------------------------------------------------------- /project_linters/go/revive.vim: -------------------------------------------------------------------------------- 1 | let s:revive = copy(project_lint#base_linter#get()) 2 | let s:revive.name = 'revive' 3 | let s:revive.filetype = ['go'] 4 | let s:revive.cmd_args = '-formatter json' 5 | let s:revive.format = 'json' 6 | 7 | function! s:revive.check_executable() abort 8 | if executable('revive') 9 | return self.set_cmd('revive') 10 | endif 11 | 12 | return self.set_cmd('') 13 | endfunction 14 | 15 | function! s:revive.command() abort 16 | return printf('%s %s %s/...', self.cmd, self.cmd_args, g:project_lint#root) 17 | endfunction 18 | 19 | function! s:revive.parse(item) abort 20 | if empty(a:item) || type(a:item) !=? type({}) 21 | return {} 22 | endif 23 | 24 | 25 | let l:severity = get(a:item, 'Severity', 'error') 26 | let l:file = project_lint#utils#get_nested_key(a:item, 'Position.Start.Filename') 27 | 28 | if empty(l:file) 29 | return {} 30 | endif 31 | 32 | if l:severity ==? 'warning' 33 | return self.warning(l:file) 34 | endif 35 | 36 | return self.error(l:file) 37 | endfunction 38 | 39 | call g:project_lint#linters.add(s:revive.new()) 40 | -------------------------------------------------------------------------------- /project_linters/javascript/eslint.vim: -------------------------------------------------------------------------------- 1 | let s:eslint = copy(project_lint#base_linter#get()) 2 | let s:eslint.name = 'eslint' 3 | let s:eslint.filetype = ['javascript', 'javascript.jsx', 'typescript'] 4 | let s:eslint.cmd_args = '--format=unix' 5 | let s:eslint.ft_extensions = { 6 | \ 'javascript': '.js', 7 | \ 'javascript.jsx': '.jsx', 8 | \ 'typescript': '.ts' 9 | \ } 10 | 11 | function! s:eslint.check_executable() abort 12 | let l:local = printf('%s/node_modules/.bin/eslint', g:project_lint#root) 13 | let l:global = 'eslint' 14 | 15 | if executable(l:local) 16 | return self.set_cmd(l:local) 17 | endif 18 | 19 | if executable(l:global) 20 | return self.set_cmd(l:global) 21 | endif 22 | 23 | return self.set_cmd('') 24 | endfunction 25 | 26 | function! s:eslint.parse(item) abort 27 | return project_lint#parsers#unix_with_severity( 28 | \ a:item, 29 | \ '^.*\s\[\(Warning\|Error\)\/[^\]]*\]$', 30 | \ 'Warning' 31 | \ ) 32 | endfunction 33 | 34 | function! s:eslint.command() abort 35 | let l:args = '' 36 | for l:filetype in self.enabled_filetype 37 | let l:args .= printf('--ext="%s" ', self.ft_extensions[l:filetype]) 38 | endfor 39 | 40 | return printf('%s %s %s %s', self.cmd, l:args, self.cmd_args, g:project_lint#root) 41 | endfunction 42 | 43 | let s:instance = s:eslint.new() 44 | 45 | function project_linters#javascript#eslint#get() abort 46 | return s:instance 47 | endfunction 48 | 49 | call g:project_lint#linters.add(s:instance) 50 | -------------------------------------------------------------------------------- /project_linters/lua/luac.vim: -------------------------------------------------------------------------------- 1 | let s:luac = copy(project_lint#base_linter#get()) 2 | let s:luac.name = 'luac' 3 | let s:luac.stream = 'stderr' 4 | let s:luac.filetype = ['lua'] 5 | let s:luac.cmd_args = '-p' 6 | 7 | function! s:luac.check_executable() abort 8 | if executable('luac') 9 | return self.set_cmd('luac') 10 | endif 11 | 12 | return self.set_cmd('') 13 | endfunction 14 | 15 | function! s:luac.command() abort 16 | return printf('%s %s %s/**/*.lua', self.cmd, self.cmd_args, g:project_lint#root) 17 | endfunction 18 | 19 | function! s:luac.parse(item) abort 20 | let l:path = project_lint#parsers#unix(substitute(a:item, '^luac:\s', '', '')) 21 | if empty(l:path) 22 | return {} 23 | endif 24 | 25 | return self.error(l:path) 26 | endfunction 27 | 28 | call g:project_lint#linters.add(s:luac.new()) 29 | -------------------------------------------------------------------------------- /project_linters/lua/luacheck.vim: -------------------------------------------------------------------------------- 1 | let s:luacheck = copy(project_lint#base_linter#get()) 2 | let s:luacheck.name = 'luacheck' 3 | let s:luacheck.filetype = ['lua'] 4 | let s:luacheck.cmd_args = '--formatter plain --codes --no-color' 5 | 6 | function! s:luacheck.check_executable() abort 7 | if executable('luacheck') 8 | return self.set_cmd('luacheck') 9 | endif 10 | 11 | return self.set_cmd('') 12 | endfunction 13 | 14 | function! s:luacheck.parse(item) abort 15 | return project_lint#parsers#unix_with_severity( 16 | \ a:item, 17 | \ '^.*:\d\+:\d\+: (\([WE]\)\d\+) .\+$', 18 | \ 'W' 19 | \ ) 20 | endfunction 21 | 22 | call g:project_lint#linters.add(s:luacheck.new()) 23 | -------------------------------------------------------------------------------- /project_linters/php/php.vim: -------------------------------------------------------------------------------- 1 | let s:php = copy(project_lint#base_linter#get()) 2 | let s:php.name = 'php' 3 | let s:php.stream = 'stderr' 4 | let s:php.filetype = ['php'] 5 | let s:php.cmd_args = '-l' 6 | 7 | function! s:php.check_executable() abort 8 | if executable('php') 9 | return self.set_cmd('php') 10 | endif 11 | 12 | return self.set_cmd('') 13 | endfunction 14 | 15 | function! s:php.command() abort 16 | return project_lint#utils#xargs_lint_command('php', self.cmd, self.cmd_args) 17 | endfunction 18 | 19 | function! s:php.parse(item) abort 20 | let l:pattern = '\v^.*(Parse|Fatal) error:.*in (.*\.php) on.*$' 21 | 22 | if a:item !~? l:pattern 23 | return {} 24 | endif 25 | 26 | let l:matches = matchlist(a:item, l:pattern) 27 | 28 | 29 | if len(l:matches) < 3 || empty(l:matches[2]) 30 | return {} 31 | endif 32 | 33 | if stridx(l:matches[2], g:project_lint#root) > -1 34 | return self.error(l:matches[2]) 35 | endif 36 | 37 | return self.error(printf('%s/%s', g:project_lint#root, l:matches[2])) 38 | endfunction 39 | 40 | call g:project_lint#linters.add(s:php.new()) 41 | -------------------------------------------------------------------------------- /project_linters/python/flake8.vim: -------------------------------------------------------------------------------- 1 | let s:flake8 = copy(project_lint#base_linter#get()) 2 | let s:flake8.name = 'flake8' 3 | let s:flake8.filetype = ['python'] 4 | 5 | function! s:flake8.check_executable() abort 6 | if executable('flake8') 7 | return self.set_cmd('flake8') 8 | endif 9 | 10 | return self.set_cmd('') 11 | endfunction 12 | 13 | function! s:flake8.parse(item) abort 14 | return project_lint#parsers#unix_with_severity( 15 | \ a:item, 16 | \ '^[^:]*:\d\+:\d\+: \(.\).*$', 17 | \ 'W' 18 | \ ) 19 | endfunction 20 | 21 | call g:project_lint#linters.add(s:flake8.new()) 22 | -------------------------------------------------------------------------------- /project_linters/python/mypy.vim: -------------------------------------------------------------------------------- 1 | let s:mypy = copy(project_lint#base_linter#get()) 2 | let s:mypy.name = 'mypy' 3 | let s:mypy.filetype = ['python'] 4 | 5 | function! s:mypy.detect() abort 6 | return !empty(self.cmd) && !empty(project_lint#utils#find_extension('py')) 7 | endfunction 8 | 9 | function! s:mypy.check_executable() abort 10 | if executable('mypy') 11 | return self.set_cmd('mypy') 12 | endif 13 | 14 | return self.set_cmd('') 15 | endfunction 16 | 17 | function! s:mypy.command() abort 18 | return printf('%s %s %s/**/*.py', self.cmd, self.cmd_args, g:project_lint#root) 19 | endfunction 20 | 21 | function! s:mypy.parse(item) abort 22 | return project_lint#parsers#unix_with_severity( 23 | \ a:item, 24 | \ '^[^:]*:\d*: \(error\)\?.*$', 25 | \ 'error', 26 | \ v:true 27 | \ ) 28 | endfunction 29 | 30 | call g:project_lint#linters.add(s:mypy.new()) 31 | -------------------------------------------------------------------------------- /project_linters/ruby/rubocop.vim: -------------------------------------------------------------------------------- 1 | let s:rubocop = copy(project_lint#base_linter#get()) 2 | let s:rubocop.name = 'rubocop' 3 | let s:rubocop.filetype = ['ruby'] 4 | let s:rubocop.cmd_args = '--no-color -l --format json' 5 | 6 | function! s:rubocop.check_executable() abort 7 | if executable('rubocop') 8 | return self.set_cmd('rubocop') 9 | endif 10 | 11 | return self.set_cmd('') 12 | endfunction 13 | 14 | function! s:rubocop.parse(item) abort 15 | let l:has_error = v:false 16 | let l:warnings = ['convention', 'warning', 'refactor'] 17 | 18 | for l:offense in a:item.offenses 19 | if index(l:warnings, l:offense.severity) < 0 20 | let l:has_error = v:true 21 | endif 22 | endfor 23 | 24 | let l:path = printf('%s/%s', g:project_lint#root, a:item.path) 25 | 26 | if l:has_error 27 | return self.error(l:path) 28 | endif 29 | 30 | return self.warning(l:path) 31 | endfunction 32 | 33 | function! s:rubocop.parse_messages(messages) abort 34 | if empty(a:messages[0]) 35 | return [] 36 | endif 37 | 38 | try 39 | let l:data = json_decode(a:messages[0]) 40 | catch 41 | return [] 42 | endtry 43 | 44 | if type(l:data) !=? type({}) 45 | return [] 46 | endif 47 | 48 | return get(l:data, 'files', []) 49 | endfunction 50 | 51 | call g:project_lint#linters.add(s:rubocop.new()) 52 | -------------------------------------------------------------------------------- /project_linters/ruby/ruby.vim: -------------------------------------------------------------------------------- 1 | let s:ruby = copy(project_lint#base_linter#get()) 2 | let s:ruby.name = 'ruby' 3 | let s:ruby.stream = 'stderr' 4 | let s:ruby.filetype = ['ruby'] 5 | let s:ruby.cmd_args = '-w -c -T1' 6 | 7 | function! s:ruby.check_executable() abort 8 | if executable('ruby') 9 | return self.set_cmd('ruby') 10 | endif 11 | 12 | return self.set_cmd('') 13 | endfunction 14 | 15 | function! s:ruby.command() abort 16 | return project_lint#utils#xargs_lint_command('rb', self.cmd, self.cmd_args) 17 | endfunction 18 | 19 | function! s:ruby.parse(item) abort 20 | return project_lint#parsers#unix_with_severity( 21 | \ a:item, 22 | \ '^[^:]*:\d*: \(warning\)\?.*$', 23 | \ 'warning' 24 | \ ) 25 | endfunction 26 | 27 | call project_lint#linters.add(s:ruby.new()) 28 | -------------------------------------------------------------------------------- /project_linters/rust/rustc.vim: -------------------------------------------------------------------------------- 1 | let s:rustc = copy(project_lint#base_linter#get()) 2 | let s:rustc.name = 'rustc' 3 | let s:rustc.stream = 'stderr' 4 | let s:rustc.filetype = ['rust'] 5 | let s:rustc.cmd_args = '--color never --error-format short' 6 | 7 | function! s:rustc.check_executable() abort 8 | if executable('rustc') 9 | return self.set_cmd('rustc') 10 | endif 11 | 12 | return self.set_cmd('') 13 | endfunction 14 | 15 | function! s:rustc.command() abort 16 | return project_lint#utils#xargs_lint_command('rs', self.cmd, self.cmd_args) 17 | endfunction 18 | 19 | function! s:rustc.parse(item) abort 20 | return project_lint#parsers#unix_with_severity( 21 | \ a:item, 22 | \ '^[^:]*:\d*:\d*: \(error\|warning\).*$', 23 | \ 'warning' 24 | \ ) 25 | endfunction 26 | 27 | call g:project_lint#linters.add(s:rustc.new()) 28 | -------------------------------------------------------------------------------- /project_linters/sass/stylelint.vim: -------------------------------------------------------------------------------- 1 | runtime! project_linters/css/stylelint.vim 2 | call g:project_lint#linters.add(project_linters#css#stylelint#get()) 3 | -------------------------------------------------------------------------------- /project_linters/scss/stylelint.vim: -------------------------------------------------------------------------------- 1 | runtime! project_linters/css/stylelint.vim 2 | call g:project_lint#linters.add(project_linters#css#stylelint#get()) 3 | -------------------------------------------------------------------------------- /project_linters/typescript/eslint.vim: -------------------------------------------------------------------------------- 1 | runtime! project_linters/javascript/eslint.vim 2 | call g:project_lint#linters.add(project_linters#javascript#eslint#get()) 3 | -------------------------------------------------------------------------------- /project_linters/typescript/tslint.vim: -------------------------------------------------------------------------------- 1 | let s:tslint = copy(project_lint#base_linter#get()) 2 | let s:tslint.name = 'tslint' 3 | let s:tslint.filetype = ['typescript'] 4 | let s:tslint.cmd_args = '--outputAbsolutePaths --format json' 5 | let s:tslint.format = 'json' 6 | 7 | function! s:tslint.check_executable() abort 8 | let l:local = printf('%s/node_modules/.bin/tslint', g:project_lint#root) 9 | let l:global = 'tslint' 10 | if filereadable(printf('%s/tslint.json', g:project_lint#root)) && self.cmd_args !~? 'tslint.json' 11 | let self.cmd_args .= ' -c tslint.json' 12 | endif 13 | 14 | if executable(l:local) 15 | return self.set_cmd(l:local) 16 | endif 17 | 18 | if executable(l:global) 19 | return self.set_cmd(l:global) 20 | endif 21 | 22 | return self.set_cmd('') 23 | endfunction 24 | 25 | function! s:tslint.command() abort 26 | let l:ext_cmd = project_lint#utils#find_extension_cmd('ts') 27 | return printf('%s | grep -v node_modules | xargs %s %s', l:ext_cmd, self.cmd, self.cmd_args) 28 | endfunction 29 | 30 | function! s:tslint.parse(item) abort 31 | if empty(a:item) 32 | return {} 33 | endif 34 | 35 | let l:file = get(a:item, 'name', '') 36 | let l:severity = get(a:item, 'ruleSeverity', 'ERROR') 37 | 38 | if empty(l:file) 39 | return {} 40 | endif 41 | 42 | if l:severity ==? 'WARNING' 43 | return self.warning(l:file) 44 | endif 45 | 46 | return self.error(l:file) 47 | endfunction 48 | 49 | call g:project_lint#linters.add(s:tslint.new()) 50 | -------------------------------------------------------------------------------- /project_linters/vim/vint.vim: -------------------------------------------------------------------------------- 1 | let s:vint = copy(project_lint#base_linter#get()) 2 | let s:vint.name = 'vint' 3 | let s:vint.filetype = ['vim'] 4 | let s:vint.cmd_args = printf('-w -f "{file_path}:{line_number}:{severity}" %s', has('nvim') ? ' --enable-neovim' : '') 5 | 6 | function! s:vint.check_executable() abort 7 | if executable('vint') 8 | return self.set_cmd('vint') 9 | endif 10 | 11 | return self.set_cmd('') 12 | endfunction 13 | 14 | function! s:vint.parse(item) abort 15 | return project_lint#parsers#unix_with_severity( 16 | \ a:item, 17 | \ '^[^:]*:\d*:\(.*\)$', 18 | \ 'warning' 19 | \ ) 20 | endfunction 21 | 22 | call g:project_lint#linters.add(s:vint.new()) 23 | -------------------------------------------------------------------------------- /rplugin/python3/defx/column/project_lint.py: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # FILE: project_lint.py 3 | # AUTHOR: Kristijan Husak 4 | # License: MIT license 5 | # ============================================================================ 6 | 7 | import typing 8 | from defx.base.column import Base 9 | from defx.context import Context 10 | from neovim import Nvim 11 | 12 | 13 | class Column(Base): 14 | 15 | def __init__(self, vim: Nvim) -> None: 16 | super().__init__(vim) 17 | self.name = 'project_lint' 18 | self.column_length = self.vim.vars['project_lint#defx_column_length'] 19 | self.error_icon: str = self.vim.vars['project_lint#error_icon'] 20 | self.error_color: str = self.vim.vars['project_lint#error_icon_color'] 21 | self.warning_icon: str = self.vim.vars['project_lint#warning_icon'] 22 | self.warning_color: str = self.vim.vars[ 23 | 'project_lint#warning_icon_color' 24 | ] 25 | self.cache: typing.Dict[str, dict] = {} 26 | 27 | def get(self, context: Context, candidate: dict) -> str: 28 | default: str = self.format('') 29 | if candidate.get('is_root', False): 30 | self.cache = self.vim.call('project_lint#get_data') 31 | return default 32 | 33 | if not self.cache: 34 | return default 35 | 36 | path = str(candidate['action__path']) 37 | 38 | if path not in self.cache: 39 | return default 40 | 41 | if self.cache[path].get('e', 0): 42 | return self.format(self.error_icon) 43 | 44 | if self.cache[path].get('w', 0): 45 | return self.format(self.warning_icon) 46 | 47 | return default 48 | 49 | def length(self, context: Context) -> int: 50 | return self.column_length 51 | 52 | def format(self, column: str) -> str: 53 | return format(column, f'<{self.column_length}') 54 | 55 | def highlight(self) -> None: 56 | self.vim.command(('syntax match {0}_{1} /[{2}]/ ' + 57 | 'contained containedin={0}').format( 58 | self.syntax_name, 'error', self.error_icon 59 | )) 60 | self.vim.command('highlight default {0}_{1} {2}'.format( 61 | self.syntax_name, 'error', self.error_color 62 | )) 63 | self.vim.command(('syntax match {0}_{1} /[{2}]/ ' + 64 | 'contained containedin={0}').format( 65 | self.syntax_name, 'warning', self.warning_icon 66 | )) 67 | self.vim.command('highlight default {0}_{1} {2}'.format( 68 | self.syntax_name, 'warning', self.warning_color 69 | )) 70 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -d "vim-themis" ]; then 4 | git clone https://github.com/thinca/vim-themis 5 | fi 6 | 7 | ./vim-themis/bin/themis -r --reporter spec test/ 8 | -------------------------------------------------------------------------------- /test/data_test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('data') 2 | let s:assert = themis#helper('assert') 3 | 4 | let s:cache_dir = expand(':p:h') 5 | let s:cache_file = printf('%s/%s.json', s:cache_dir, tolower(substitute(getcwd(), '/', '-', 'g'))[1:]) 6 | let s:old_cache_dir = g:project_lint#cache_dir 7 | 8 | function! s:suite.before() abort 9 | if filereadable(s:cache_file) 10 | call delete(s:cache_file) 11 | endif 12 | endfunction 13 | 14 | function! s:suite.after() abort 15 | if filereadable(s:cache_file) 16 | call delete(s:cache_file) 17 | endif 18 | let g:project_lint#cache_dir = s:old_cache_dir 19 | endfunction 20 | 21 | let s:data = project_lint#data#new() 22 | 23 | function s:get_path(path, type) abort 24 | return { 'path': printf('%s/%s', getcwd(), a:path), 'severity': a:type } 25 | endfunction 26 | 27 | let s:linter = {'name': 'vinter'} 28 | 29 | function! s:suite.should_be_empty_on_start() abort 30 | call s:assert.empty(s:data.get()) 31 | endfunction 32 | 33 | function! s:suite.should_add_file_and_all_its_parents_folders_until_root() abort 34 | call s:data.add(s:linter, s:get_path('nested/path/inside/folders/myfile.vim', 'e')) 35 | call s:assert.has_key(s:data.get(), s:get_path('nested', 'e').path) 36 | call s:assert.equals(s:data.get_item(s:get_path('nested', 'e').path), {'vinter': {'e': 1, 'w': 0 }}) 37 | call s:assert.has_key(s:data.get(), s:get_path('nested/path', 'e').path) 38 | call s:assert.equals(s:data.get_item(s:get_path('nested/path', 'e').path), {'vinter': {'e': 1, 'w': 0 }}) 39 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside', 'e').path) 40 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside', 'e').path), {'vinter': { 'e': 1, 'w': 0 }}) 41 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside/folders', 'e').path) 42 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders', 'e').path), {'vinter': { 'e': 1, 'w': 0 }}) 43 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside/folders/myfile.vim', 'e').path) 44 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders/myfile.vim', 'e').path), {'vinter': { 'e': 1, 'w': 0 }}) 45 | 46 | call s:data.add(s:linter, s:get_path('nested/path/inside/folders/myfile.vim', 'w')) 47 | call s:assert.has_key(s:data.get(), s:get_path('nested', 'w').path) 48 | call s:assert.equals(s:data.get_item(s:get_path('nested', 'w').path), {'vinter': {'e': 1, 'w': 1 }}) 49 | call s:assert.has_key(s:data.get(), s:get_path('nested/path', 'w').path) 50 | call s:assert.equals(s:data.get_item(s:get_path('nested/path', 'w').path), {'vinter': {'e': 1, 'w': 1 }}) 51 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside', 'w').path) 52 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside', 'w').path), {'vinter': { 'e': 1, 'w': 1 }}) 53 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside/folders', 'w').path) 54 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders', 'w').path), {'vinter': { 'e': 1, 'w': 1 }}) 55 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside/folders/myfile.vim', 'w').path) 56 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders/myfile.vim', 'w').path), {'vinter': { 'e': 1, 'w': 1 }}) 57 | 58 | call s:data.add_total_severity_counters(s:get_path('nested/path/inside/folders/myfile.vim', 'w').path) 59 | call s:assert.equals(s:data.get_item(s:get_path('nested', 'w').path), {'vinter': {'e': 1, 'w': 1 }, 'e': 1, 'w': 1 }) 60 | call s:assert.equals(s:data.get_item(s:get_path('nested/path', 'w').path), {'vinter': {'e': 1, 'w': 1 }, 'e': 1, 'w': 1 }) 61 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside', 'w').path), {'vinter': { 'e': 1, 'w': 1 }, 'e': 1, 'w': 1 }) 62 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders', 'w').path), {'vinter': { 'e': 1, 'w': 1 }, 'e': 1, 'w': 1 }) 63 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders/myfile.vim', 'w').path), {'vinter': { 'e': 1, 'w': 1 }, 'e': 1, 'w': 1 }) 64 | endfunction 65 | 66 | function! s:suite.should_remove_file_and_all_its_parents_folders_until_root() abort 67 | call s:data.remove(s:linter, s:get_path('nested/path/inside/folders/myfile.vim', 'e').path) 68 | call s:assert.key_not_exists(s:data.get(), s:get_path('nested', 'e').path) 69 | call s:assert.key_not_exists(s:data.get(), s:get_path('nested/path', 'e').path) 70 | call s:assert.key_not_exists(s:data.get(), s:get_path('nested/path/inside', 'e').path) 71 | call s:assert.key_not_exists(s:data.get(), s:get_path('nested/path/inside/folders', 'e').path) 72 | call s:assert.key_not_exists(s:data.get(), s:get_path('nested/path/inside/folders/myfile.vim', 'e').path) 73 | endfunction 74 | " 75 | function! s:suite.should_not_add_a_file_if_not_part_of_project_root() abort 76 | call s:data.add(s:linter, {'path': '/some/invalid/file/path.js', 'type': 'e'}) 77 | call s:assert.key_not_exists(s:data.get(), '/some/invalid/file/path.js') 78 | endfunction 79 | " 80 | function! s:suite.should_add_file_and_all_its_parents_folders_until_root_and_cache_to_file() abort 81 | call s:data.add(s:linter, s:get_path('nested/path/inside/folders/myfile.vim', 'e')) 82 | call s:data.add_total_severity_counters(s:get_path('nested/path/inside/folders/myfile.vim', 'w').path) 83 | call s:assert.has_key(s:data.get(), s:get_path('nested', 'e').path) 84 | call s:assert.equals(s:data.get_item(s:get_path('nested', 'e').path), {'vinter': {'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 85 | call s:assert.has_key(s:data.get(), s:get_path('nested/path', 'e').path) 86 | call s:assert.equals(s:data.get_item(s:get_path('nested/path', 'e').path), {'vinter': {'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 87 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside', 'e').path) 88 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside', 'e').path), {'vinter': { 'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 89 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside/folders', 'e').path) 90 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders', 'e').path), {'vinter': { 'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 91 | call s:assert.has_key(s:data.get(), s:get_path('nested/path/inside/folders/myfile.vim', 'e').path) 92 | call s:assert.equals(s:data.get_item(s:get_path('nested/path/inside/folders/myfile.vim', 'e').path), {'vinter': { 'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 93 | let g:project_lint#cache_dir = s:cache_dir 94 | call s:assert.equals(s:data.cache_filename(), s:cache_file) 95 | call s:assert.false(filereadable(s:cache_file)) 96 | call s:data.cache_to_file() 97 | call s:assert.true(filereadable(s:cache_file)) 98 | let l:file_data = json_decode(readfile(s:cache_file)[0]) 99 | call s:assert.has_key(l:file_data, s:get_path('nested', 'e').path) 100 | call s:assert.equals(l:file_data[s:get_path('nested', 'e').path], {'vinter': { 'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 101 | call s:assert.has_key(l:file_data, s:get_path('nested/path', 'e').path) 102 | call s:assert.equals(l:file_data[s:get_path('nested/path', 'e').path], {'vinter': {'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 103 | call s:assert.has_key(l:file_data, s:get_path('nested/path/inside', 'e').path) 104 | call s:assert.equals(l:file_data[s:get_path('nested/path/inside', 'e').path], {'vinter': {'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 105 | call s:assert.has_key(l:file_data, s:get_path('nested/path/inside/folders', 'e').path) 106 | call s:assert.equals(l:file_data[s:get_path('nested/path/inside/folders', 'e').path], {'vinter': {'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 107 | call s:assert.has_key(l:file_data, s:get_path('nested/path/inside/folders/myfile.vim', 'e').path) 108 | call s:assert.equals(l:file_data[s:get_path('nested/path/inside/folders/myfile.vim', 'e').path], {'vinter': {'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 109 | endfunction 110 | 111 | function! s:suite.should_use_cache_if_toggled_on() abort 112 | let s:data.paths = {'file': 1 } 113 | let s:data.cache = {'cache_file': 1 } 114 | call s:assert.equals(v:false, s:data.use_cache) 115 | call s:assert.has_key(s:data.get(), 'file') 116 | call s:assert.key_not_exists(s:data.get(), 'cache_file') 117 | let s:data.use_cache = v:true 118 | call s:assert.key_not_exists(s:data.get(), 'file') 119 | call s:assert.has_key(s:data.get(), 'cache_file') 120 | let s:data.paths = {} 121 | let s:data.cache = {} 122 | let s:data.use_cache = v:false 123 | endfunction 124 | -------------------------------------------------------------------------------- /test/file_explorers_test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('file_explorers') 2 | let s:assert = themis#helper('assert') 3 | 4 | function! s:suite.after() abort 5 | unlet! g:loaded_defx g:loaded_nerd_tree g:loaded_vimfiler 6 | endfunction 7 | 8 | let s:file_explorers = project_lint#file_explorers#new() 9 | 10 | function! s:suite.should_not_detect_any_file_explorers() abort 11 | call s:assert.equals(0, s:file_explorers.has_valid_file_explorer()) 12 | endfunction 13 | 14 | function! s:suite.should_detect_defx() abort 15 | let g:loaded_defx = 1 16 | call s:assert.equals(1, s:file_explorers.has_defx()) 17 | call s:assert.equals(1, s:file_explorers.has_valid_file_explorer()) 18 | unlet g:loaded_defx 19 | endfunction 20 | 21 | function! s:suite.should_detect_nerdtree() abort 22 | let g:loaded_nerd_tree = 1 23 | call s:assert.equals(0, s:file_explorers.has_defx()) 24 | call s:assert.equals(1, s:file_explorers.has_nerdtree()) 25 | call s:assert.equals(1, s:file_explorers.has_valid_file_explorer()) 26 | unlet g:loaded_nerd_tree 27 | endfunction 28 | 29 | function! s:suite.should_detect_vimfiler() abort 30 | let g:loaded_vimfiler = 1 31 | call s:assert.equals(0, s:file_explorers.has_defx()) 32 | call s:assert.equals(0, s:file_explorers.has_nerdtree()) 33 | call s:assert.equals(1, s:file_explorers.has_vimfiler()) 34 | call s:assert.equals(1, s:file_explorers.has_valid_file_explorer()) 35 | unlet g:loaded_vimfiler 36 | endfunction 37 | 38 | function! s:suite.should_register_callbacks() abort 39 | let g:loaded_defx = 1 40 | let g:loaded_vimfiler = 1 41 | call s:file_explorers.register() 42 | call s:assert.length_of(s:file_explorers.explorers, 2) 43 | endfunction 44 | -------------------------------------------------------------------------------- /test/linter_mock.vim: -------------------------------------------------------------------------------- 1 | function! s:get_mock() abort 2 | return s:linter_mock 3 | endfunctio 4 | 5 | function! s:get_mock_file() abort 6 | return s:file_mock 7 | endfunction 8 | 9 | let s:file_mock = printf('%s/file.js', getcwd()) 10 | let s:linter_mock = { 11 | \ 'name': 'my_linter', 12 | \ 'stream': 'stdout', 13 | \ 'filetype': ['javascript', 'javascript.jsx', 'typescript'], 14 | \ } 15 | 16 | function! s:linter_mock.command() abort 17 | return printf('echo "%s"', s:file_mock) 18 | endfunction 19 | 20 | function! s:linter_mock.file_command(file) abort 21 | return printf('echo "%s"', s:file_mock) 22 | endfunction 23 | 24 | function! s:linter_mock.check_executable() abort 25 | return v:true 26 | endfunction 27 | 28 | function! s:linter_mock.detect() abort 29 | return v:true 30 | endfunction 31 | 32 | function! s:linter_mock.parse(msg) abort 33 | return {'path': a:msg, 'severity': 'e'} 34 | return a:msg 35 | endfunction 36 | 37 | function! s:linter_mock.parse_messages(messages) abort 38 | return a:messages 39 | endfunction 40 | -------------------------------------------------------------------------------- /test/linters_test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('linters') 2 | let s:assert = themis#helper('assert') 3 | let s:scope = themis#helper('scope') 4 | let s:mock_functions = s:scope.funcs('test/linter_mock.vim') 5 | let s:linter_mock = s:mock_functions.get_mock() 6 | let s:file_mock = s:mock_functions.get_mock_file() 7 | 8 | function! s:suite.after() abort 9 | let g:project_lint#enabled_linters = {} 10 | endfunction 11 | 12 | let s:linters = project_lint#linters#new() 13 | 14 | function! s:suite.should_be_empty_on_start() abort 15 | call s:assert.empty(s:linters.items) 16 | endfunction 17 | 18 | function! s:suite.should_add_linter_to_list() abort 19 | call s:linters.add(s:linter_mock) 20 | call s:assert.length_of(s:linters.items, 1) 21 | endfunction 22 | 23 | function! s:suite.should_return_all_linter_filetypes_as_enabled_if_global_setting_for_them_is_not_defined() abort 24 | call s:assert.equals(s:linters.get_enabled_filetypes(s:linter_mock), ['javascript', 'javascript.jsx', 'typescript']) 25 | endfunction 26 | 27 | function! s:suite.should_return_only_js_and_ts_as_enabled_filetype() abort 28 | let g:project_lint#enabled_linters = {'javascript.jsx': []} 29 | call s:assert.equals(s:linters.get_enabled_filetypes(s:linter_mock), ['javascript', 'typescript']) 30 | endfunction 31 | 32 | function! s:suite.should_return_only_js_as_enabled_filetype_because_linter_not_defined_for_other_filetypes() abort 33 | let g:project_lint#enabled_linters = {'javascript.jsx': ['other_linter'], 'typescript': ['tslint'] } 34 | call s:assert.equals(s:linters.get_enabled_filetypes(s:linter_mock), ['javascript']) 35 | endfunction 36 | -------------------------------------------------------------------------------- /test/project_lint_test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('project_lint') 2 | let s:assert = themis#helper('assert') 3 | let s:scope = themis#helper('scope') 4 | let s:mock_functions = s:scope.funcs('test/linter_mock.vim') 5 | let s:linter_mock = s:mock_functions.get_mock() 6 | let s:file_mock = s:mock_functions.get_mock_file() 7 | 8 | let s:job = project_lint#job#new() 9 | let s:linters = project_lint#linters#new() 10 | call s:linters.add(s:linter_mock) 11 | let s:data = project_lint#data#new() 12 | let s:queue = project_lint#queue#new(s:job, s:data, s:linters) 13 | let s:file_explorers = project_lint#file_explorers#new() 14 | 15 | let s:project_lint = project_lint#new(s:linters, s:data, s:queue, s:file_explorers) 16 | 17 | function! s:suite.should_not_init_project_lint_if_no_file_explorers_are_detected() abort 18 | let l:result = s:project_lint.init() 19 | call s:assert.equals(s:project_lint.running, v:false) 20 | call s:assert.false(l:result) 21 | endfunction 22 | 23 | function! s:suite.should_init_if_one_of_file_explorers_are_detected() abort 24 | let g:loaded_defx = 1 25 | call s:assert.true(s:file_explorers.has_defx()) 26 | call s:assert.true(s:file_explorers.has_valid_file_explorer()) 27 | let l:result = s:project_lint.init() 28 | call s:assert.equals(s:project_lint.running, v:true) 29 | call s:assert.length_of(s:queue.list, 1) 30 | sleep 50m 31 | call s:assert.equals(s:project_lint.running, v:false) 32 | call s:assert.length_of(s:queue.list, 0) 33 | call s:assert.length_of(s:data.get(), 1) 34 | call s:assert.equals(s:data.get_item(s:file_mock), { 'my_linter': { 'e': 1, 'w': 0 }, 'e': 1, 'w': 0 }) 35 | endfunction 36 | -------------------------------------------------------------------------------- /test/queue_test.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('queue') 2 | let s:assert = themis#helper('assert') 3 | let s:scope = themis#helper('scope') 4 | let s:mock_functions = s:scope.funcs('test/linter_mock.vim') 5 | let s:linter_mock = s:mock_functions.get_mock() 6 | let s:file_mock = s:mock_functions.get_mock_file() 7 | 8 | let s:data = project_lint#data#new() 9 | let s:job = project_lint#job#new() 10 | let s:linters = project_lint#linters#new() 11 | let s:queue = project_lint#queue#new(s:job, s:data, s:linters) 12 | 13 | function! s:suite.should_be_empty_on_start() abort 14 | call s:assert.true(s:queue.is_empty()) 15 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': []}) 16 | endfunction 17 | 18 | function! s:suite.should_add_project_job() abort 19 | let l:id = s:queue.add(s:linter_mock) 20 | call s:assert.false(s:queue.is_empty()) 21 | call s:assert.equals(s:queue.get_running_linters(), {'project': ['my_linter'], 'files': []}) 22 | call s:assert.equals(v:true, s:queue.is_linting_project()) 23 | call s:assert.length_of(s:queue.list, 1) 24 | call s:assert.equals(s:queue.list[keys(s:queue.list)[0]].linter.name, s:linter_mock.name) 25 | call s:assert.empty(s:queue.list[keys(s:queue.list)[0]].file) 26 | sleep 50m 27 | call s:assert.true(s:queue.is_empty()) 28 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': []}) 29 | call s:assert.equals(v:false, s:queue.is_linting_project()) 30 | call s:assert.empty(s:queue.list) 31 | call s:assert.has_key(s:data.paths, s:file_mock) 32 | call s:assert.has_key(s:data.paths[s:file_mock], 'my_linter') 33 | call s:assert.equals(s:data.paths[s:file_mock].my_linter, {'e': 1, 'w': 0}) 34 | endfunction 35 | 36 | function! s:suite.should_add_file_job() abort 37 | let l:id = s:queue.add_file(s:linter_mock, s:file_mock) 38 | call s:assert.false(s:queue.is_empty()) 39 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': ['my_linter']}) 40 | call s:assert.equals(v:false, s:queue.is_linting_project()) 41 | call s:assert.length_of(s:queue.list, 1) 42 | call s:assert.equals(s:queue.list[keys(s:queue.list)[0]].linter.name, s:linter_mock.name) 43 | call s:assert.equals(s:file_mock, s:queue.list[keys(s:queue.list)[0]].file) 44 | call s:assert.equals(v:false, s:queue.is_linting_project()) 45 | call s:assert.equals(v:true, s:queue.already_linting_file(s:linter_mock, s:file_mock)) 46 | sleep 50m 47 | call s:assert.true(s:queue.is_empty()) 48 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': []}) 49 | call s:assert.equals(v:false, s:queue.already_linting_file(s:linter_mock, s:file_mock)) 50 | call s:assert.empty(s:queue.list) 51 | call s:assert.has_key(s:data.paths, s:file_mock) 52 | call s:assert.has_key(s:data.paths[s:file_mock], 'my_linter') 53 | call s:assert.equals(s:data.paths[s:file_mock].my_linter, {'e': 1, 'w': 0}) 54 | endfunction 55 | 56 | 57 | function! s:suite.should_mark_file_as_valid_and_remove_from_data() abort 58 | function! s:linter_mock.file_command(file) abort 59 | return 'echo ""' 60 | endfunction 61 | let l:id = s:queue.add_file(s:linter_mock, s:file_mock) 62 | call s:assert.false(s:queue.is_empty()) 63 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': ['my_linter']}) 64 | call s:assert.equals(v:false, s:queue.is_linting_project()) 65 | call s:assert.length_of(s:queue.list, 1) 66 | call s:assert.equals(s:queue.list[keys(s:queue.list)[0]].linter.name, s:linter_mock.name) 67 | call s:assert.equals(s:file_mock, s:queue.list[keys(s:queue.list)[0]].file) 68 | call s:assert.equals(v:false, s:queue.is_linting_project()) 69 | call s:assert.equals(v:true, s:queue.already_linting_file(s:linter_mock, s:file_mock)) 70 | sleep 50m 71 | call s:assert.true(s:queue.is_empty()) 72 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': []}) 73 | call s:assert.equals(v:false, s:queue.already_linting_file(s:linter_mock, s:file_mock)) 74 | call s:assert.empty(s:queue.list) 75 | call s:assert.key_not_exists(s:data.paths, s:file_mock) 76 | function! s:linter_mock.file_command(file) abort 77 | return printf('echo "%s"', s:file_mock) 78 | endfunction 79 | endfunction 80 | 81 | function! s:suite.should_add_file_to_after_project_lint_list_queue() abort 82 | let l:id = s:queue.add(s:linter_mock) 83 | let l:file_job_id = s:queue.add_file(s:linter_mock, s:file_mock) 84 | call s:assert.false(s:queue.is_empty()) 85 | call s:assert.equals(s:queue.get_running_linters(), {'project': ['my_linter'], 'files': []}) 86 | call s:assert.equals(v:true, s:queue.is_linting_project()) 87 | call s:assert.length_of(s:queue.list, 1) 88 | call s:assert.length_of(s:queue.post_project_lint_file_list, 1) 89 | call s:assert.equals(s:queue.post_project_lint_file_list[0], {'linter': s:linter_mock, 'file': s:file_mock }) 90 | call s:assert.equals(s:queue.list[keys(s:queue.list)[0]].linter.name, s:linter_mock.name) 91 | call s:assert.empty(s:queue.list[keys(s:queue.list)[0]].file) 92 | sleep 50m 93 | call s:assert.false(s:queue.is_empty()) 94 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': ['my_linter']}) 95 | call s:assert.equals(v:false, s:queue.is_linting_project()) 96 | call s:assert.length_of(s:queue.list, 1) 97 | call s:assert.equals(s:queue.list[keys(s:queue.list)[0]].linter.name, s:linter_mock.name) 98 | call s:assert.equals(s:file_mock, s:queue.list[keys(s:queue.list)[0]].file) 99 | call s:assert.equals(v:false, s:queue.is_linting_project()) 100 | call s:assert.equals(v:true, s:queue.already_linting_file(s:linter_mock, s:file_mock)) 101 | call s:assert.empty(s:queue.post_project_lint_file_list) 102 | sleep 50m 103 | call s:assert.true(s:queue.is_empty()) 104 | call s:assert.equals(s:queue.get_running_linters(), {'project': [], 'files': []}) 105 | call s:assert.equals(v:false, s:queue.already_linting_file(s:linter_mock, s:file_mock)) 106 | call s:assert.empty(s:queue.list) 107 | call s:assert.has_key(s:data.paths, s:file_mock) 108 | call s:assert.has_key(s:data.paths[s:file_mock], 'my_linter') 109 | call s:assert.equals(s:data.paths[s:file_mock].my_linter, {'e': 1, 'w': 0}) 110 | endfunction 111 | --------------------------------------------------------------------------------