diff options
| author | Mitermayer Reis <mitermayer.reis@gmail.com> | 2020-02-05 11:08:03 +1100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-02-05 11:08:03 +1100 |
| commit | 49d91743b2df43f84edd199f877d494b4d8812f4 (patch) | |
| tree | cf856d77c9960a09eb3156937aa1b896b855bed6 /autoload/prettier | |
| parent | 9eb448e45ef88e90681335fda32bcae52a09d6dc (diff) | |
| parent | b064c6ab82a3c57ea64360d762d661ad7e8ee54c (diff) | |
| download | vim-prettier-49d91743b2df43f84edd199f877d494b4d8812f4.tar.xz | |
Merge pull request #175 from prettier/release/1.x
Release/1.x
Diffstat (limited to 'autoload/prettier')
| -rw-r--r-- | autoload/prettier/bridge/parser.vim | 24 | ||||
| -rw-r--r-- | autoload/prettier/job/async/neovim.vim | 98 | ||||
| -rw-r--r-- | autoload/prettier/job/async/vim.vim | 69 | ||||
| -rw-r--r-- | autoload/prettier/job/runner.vim | 63 | ||||
| -rw-r--r-- | autoload/prettier/logging/error.vim | 12 | ||||
| -rw-r--r-- | autoload/prettier/presets/fb.vim | 13 | ||||
| -rw-r--r-- | autoload/prettier/resolver/config.vim | 98 | ||||
| -rw-r--r-- | autoload/prettier/resolver/executable.vim | 74 | ||||
| -rw-r--r-- | autoload/prettier/resolver/preset.vim | 8 | ||||
| -rw-r--r-- | autoload/prettier/utils/buffer.vim | 70 | ||||
| -rw-r--r-- | autoload/prettier/utils/quickfix.vim | 22 | ||||
| -rw-r--r-- | autoload/prettier/utils/shim.vim | 8 |
12 files changed, 559 insertions, 0 deletions
diff --git a/autoload/prettier/bridge/parser.vim b/autoload/prettier/bridge/parser.vim new file mode 100644 index 0000000..51c4069 --- /dev/null +++ b/autoload/prettier/bridge/parser.vim @@ -0,0 +1,24 @@ +" TODO +" this function should just returns the parsed errors list instead +" of opening the quickfix +function! prettier#bridge#parser#onError(out, autoFocus) abort + let l:errors = [] + + for l:line in a:out + " matches: + " file.ext: SyntaxError: Unexpected token (2:8)sd + " stdin: SyntaxError: Unexpected token (2:8) + " [error] file.ext: SyntaxError: Unexpected token (2:8) + let l:match = matchlist(l:line, '^.*: \(.*\) (\(\d\{1,}\):\(\d\{1,}\)*)') + if !empty(l:match) + call add(l:errors, { 'bufnr': bufnr('%'), + \ 'text': l:match[1], + \ 'lnum': l:match[2], + \ 'col': l:match[3] }) + endif + endfor + + if len(l:errors) + call prettier#utils#quickfix#open(l:errors, a:autoFocus) + endif +endfunction diff --git a/autoload/prettier/job/async/neovim.vim b/autoload/prettier/job/async/neovim.vim new file mode 100644 index 0000000..28ecb8a --- /dev/null +++ b/autoload/prettier/job/async/neovim.vim @@ -0,0 +1,98 @@ +let s:prettier_job_running = 0 + +function! prettier#job#async#neovim#run(cmd, startSelection, endSelection) abort + if s:prettier_job_running == 1 + return + endif + let s:prettier_job_running = 1 + + let l:lines = getline(a:startSelection, a:endSelection) + let l:dict = { + \ 'start': a:startSelection - 1, + \ 'end': a:endSelection, + \ 'buf_nr': bufnr('%'), + \ 'content': l:lines, + \} + let l:out = [] + let l:err = [] + + let l:job = jobstart([&shell, &shellcmdflag, a:cmd], { + \ 'stdout_buffered': 1, + \ 'stderr_buffered': 1, + \ 'on_stdout': {job_id, data, event -> extend(l:out, data)}, + \ 'on_stderr': {job_id, data, event -> extend(l:err, data)}, + \ 'on_exit': {job_id, status, event -> s:onExit(status, l:dict, l:out, l:err)}, + \ }) + call jobsend(l:job, l:lines) + call jobclose(l:job, 'stdin') +endfunction + +" todo +" Lets refactor this onExit to work similar to our solution for vim8 +" at the moment an info json object is been passed with some cached data +" that is than used to do the formatting, its not following the same convetion +" as the vim8 one and is error prone +" +" we should: +" +" 1. make it similar to the vim8 approach +" 2. extract common functionality either above to the runner or to some other module +" +" to test this it rellies on using nvim and having the flag +" +" note that somehow we exectuing both async and sync on nvim when using the autoformat +function! s:onExit(status, info, out, err) abort + let l:currentBufferNumber = bufnr('%') + let l:isInsideAnotherBuffer = a:info.buf_nr != l:currentBufferNumber ? 1 : 0 + let l:last = a:out[len(a:out) - 1] + let l:out = l:last ==? '' ? a:out[0:len(a:out) - 2] : a:out + + " parsing errors + if a:status != 0 + call prettier#job#runner#onError(a:err) + let s:prettier_job_running = 0 + return + endif + + " we have no prettier output so lets exit + if len(a:out) == 0 | return | endif + + " nothing to update + if (prettier#utils#buffer#willUpdatedLinesChangeBuffer(l:out, a:info.start, a:info.end) == 0) + let s:prettier_job_running = 0 + return + endif + + " This is required due to race condition when user quickly switch buffers while the async + " cli has not finished running, vim 8.0.1039 has introduced setbufline() which can be used + " to fix this issue in a cleaner way, however since we still need to support older vim versions + " we will apply a more generic solution + if (l:isInsideAnotherBuffer) + " Do no try to format buffers that have been closed + if (bufloaded(a:info.buf_nr)) + try + silent exec 'sp '. escape(bufname(a:info.buf_nr), ' \') + call nvim_buf_set_lines(a:info.buf_nr, a:info.start, a:info.end, 0, l:out) + noautocmd write + catch + call prettier#logging#error#log('PARSING_ERROR') + finally + " we should then hide this buffer again + if a:info.buf_nr == bufnr('%') + silent hide + endif + endtry + endif + else + " TODO + " move this to the buffer util and let it abstract away the saving buffer + " from here + " + " TODO + " we should be auto saving in order to work similar to vim8 + call nvim_buf_set_lines(a:info.buf_nr, a:info.start, a:info.end, 0, l:out) + noautocmd write + endif + + let s:prettier_job_running = 0 +endfunction diff --git a/autoload/prettier/job/async/vim.vim b/autoload/prettier/job/async/vim.vim new file mode 100644 index 0000000..29774b9 --- /dev/null +++ b/autoload/prettier/job/async/vim.vim @@ -0,0 +1,69 @@ +let s:prettier_job_running = 0 + +function! prettier#job#async#vim#run(cmd, startSelection, endSelection) abort + if s:prettier_job_running == 1 + return + endif + let s:prettier_job_running = 1 + + let l:bufferName = bufname('%') + + let l:job = job_start([&shell, &shellcmdflag, a:cmd], { + \ 'out_io': 'buffer', + \ 'err_cb': {channel, msg -> s:onError(msg)}, + \ 'close_cb': {channel -> s:onClose(channel, a:startSelection, a:endSelection, l:bufferName)}}) + + let l:stdin = job_getchannel(l:job) + + call ch_sendraw(l:stdin, join(getbufline(bufnr(l:bufferName), a:startSelection, a:endSelection), "\n")) + call ch_close_in(l:stdin) +endfunction + +function! s:onError(msg) abort + call prettier#job#runner#onError(split(a:msg, '\n')) + let s:prettier_job_running = 0 +endfunction + +function! s:onClose(channel, startSelection, endSelection, bufferName) abort + let l:out = [] + let l:currentBufferName = bufname('%') + let l:isInsideAnotherBuffer = a:bufferName != l:currentBufferName ? 1 : 0 + + let l:buff = ch_getbufnr(a:channel, 'out') + let l:out = getbufline(l:buff, 2, '$') + execute 'bd!' . l:buff + + " we have no prettier output so lets exit + if len(l:out) == 0 | return | endif + + " nothing to update + if (prettier#utils#buffer#willUpdatedLinesChangeBuffer(l:out, a:startSelection, a:endSelection) == 0) + let s:prettier_job_running = 0 + redraw! + return + endif + + " This is required due to race condition when user quickly switch buffers while the async + " cli has not finished running, vim 8.0.1039 has introduced setbufline() which can be used + " to fix this issue in a cleaner way, however since we still need to support older vim versions + " we will apply a more generic solution + if (l:isInsideAnotherBuffer) + " Do no try to format buffers that have been closed + if (bufloaded(str2nr(a:bufferName))) + try + silent exec 'sp '. escape(bufname(bufnr(a:bufferName)), ' \') + call prettier#utils#buffer#replaceAndSave(l:out, a:startSelection, a:endSelection) + catch + call prettier#logging#error#log('PARSING_ERROR', a:bufferName) + finally + " we should then hide this buffer again + if a:bufferName == bufname('%') + silent hide + endif + endtry + endif + else + call prettier#utils#buffer#replaceAndSave(l:out, a:startSelection, a:endSelection) + endif + let s:prettier_job_running = 0 +endfunction diff --git a/autoload/prettier/job/runner.vim b/autoload/prettier/job/runner.vim new file mode 100644 index 0000000..60cb8a5 --- /dev/null +++ b/autoload/prettier/job/runner.vim @@ -0,0 +1,63 @@ +" TODO +" move the bellow vim checks to UTILS +let s:isLegacyVim = v:version < 800 +let s:isNeoVim = has('nvim') +let s:isAsyncVim = !s:isLegacyVim && exists('*job_start') + +function! prettier#job#runner#run(cmd, startSelection, endSelection, async) abort + if a:async && (s:isAsyncVim || s:isNeoVim) + call s:asyncFormat(a:cmd, a:startSelection, a:endSelection) + else + call s:format(a:cmd, a:startSelection, a:endSelection) + endif +endfunction + +function! prettier#job#runner#onError(errors) abort + call prettier#logging#error#log('PARSING_ERROR') + if g:prettier#quickfix_enabled + call prettier#bridge#parser#onError(a:errors, g:prettier#quickfix_auto_focus) + endif +endfunction + +function! s:asyncFormat(cmd, startSelection, endSelection) abort + if !s:isAsyncVim && !s:isNeoVim + call s:format(a:cmd, a:startSelection, a:endSelection) + endif + + " required for Windows support on async operations + let l:cmd = a:cmd + if has('win32') || has('win64') + let l:cmd = 'cmd.exe /c ' . a:cmd + endif + + if s:isAsyncVim + call prettier#job#async#vim#run(l:cmd, a:startSelection, a:endSelection) + else + call prettier#job#async#neovim#run(l:cmd, a:startSelection, a:endSelection) + endif +endfunction + +function! s:format(cmd, startSelection, endSelection) abort + let l:bufferLinesList = getbufline(bufnr('%'), a:startSelection, a:endSelection) + + " vim 7 does not have support for passing a list to system() + let l:bufferLines = s:isLegacyVim ? join(l:bufferLinesList, "\n") : l:bufferLinesList + + " TODO + " since we are using two different types for system, maybe we should move it to utils shims + let l:out = split(system(a:cmd, l:bufferLines), '\n') + + " check system exit code + if v:shell_error + call prettier#job#runner#onError(l:out) + return + endif + + " TODO + " doing 0 checks seems weird can we do this bellow differently ? + if (prettier#utils#buffer#willUpdatedLinesChangeBuffer(l:out, a:startSelection, a:endSelection) == 0) + return + endif + + call prettier#utils#buffer#replace(l:out, a:startSelection, a:endSelection) +endfunction diff --git a/autoload/prettier/logging/error.vim b/autoload/prettier/logging/error.vim new file mode 100644 index 0000000..48ad048 --- /dev/null +++ b/autoload/prettier/logging/error.vim @@ -0,0 +1,12 @@ +let s:PREFIX_MSG = 'Prettier: ' +let s:ERRORS = { + \ 'EXECUTABLE_NOT_FOUND_ERROR': 'no prettier executable installation found', + \ 'PARSING_ERROR': 'failed to parse buffer', + \ } +let s:DEFAULT_ERROR = get(s:, 'PARSING_ERROR') + +function! prettier#logging#error#log(...) abort + let l:error = a:0 > 0 ? a:1 : s:DEFAULT_ERROR + let l:msg = a:0 > 1 ? ': ' . a:2 : '' + echohl WarningMsg | echom s:PREFIX_MSG . get(s:ERRORS, l:error, s:DEFAULT_ERROR) . l:msg | echohl NONE +endfunction diff --git a/autoload/prettier/presets/fb.vim b/autoload/prettier/presets/fb.vim new file mode 100644 index 0000000..4715b88 --- /dev/null +++ b/autoload/prettier/presets/fb.vim @@ -0,0 +1,13 @@ +" Return facebook style config overwrite presets +function! prettier#presets#fb#config() abort + return { + \ 'bracketSpacing': 'false', + \ 'jsxBracketSameLine': 'true', + \ 'printWidth': 80, + \ 'parser': 'flow', + \ 'singleQuote': 'true', + \ 'tabWidth': 2, + \ 'trailingComma': 'all', + \ 'useTabs': 'false', + \ } +endfunction diff --git a/autoload/prettier/resolver/config.vim b/autoload/prettier/resolver/config.vim new file mode 100644 index 0000000..b22f11a --- /dev/null +++ b/autoload/prettier/resolver/config.vim @@ -0,0 +1,98 @@ +" By default we will default to our internal +" configuration settings for prettier +function! prettier#resolver#config#resolve(config, hasSelection, start, end) abort + " Allow params to be passed as json format + " convert bellow usage of globals to a get function o the params defaulting to global + " TODO: Use a list, filter() and join() to get a nicer list of args. + let l:cmd = s:Flag_use_tabs(a:config) . ' ' . + \ s:Flag_tab_width(a:config) . ' ' . + \ s:Flag_print_width(a:config) . ' ' . + \ s:Flag_parser(a:config) . ' ' . + \ s:Flag_range_delimiter(a:config, a:hasSelection, a:start, a:end) . ' ' . + \ ' --semi=' . + \ get(a:config, 'semi', g:prettier#config#semi) . + \ ' --single-quote=' . + \ get(a:config, 'singleQuote', g:prettier#config#single_quote) . + \ ' --bracket-spacing=' . + \ get(a:config, 'bracketSpacing', g:prettier#config#bracket_spacing) . + \ ' --jsx-bracket-same-line=' . + \ get(a:config, 'jsxBracketSameLine', g:prettier#config#jsx_bracket_same_line) . + \ ' --arrow-parens=' . + \ get(a:config, 'arrowParens', g:prettier#config#arrow_parens) . + \ ' --trailing-comma=' . + \ get(a:config, 'trailingComma', g:prettier#config#trailing_comma) . + \ ' --config-precedence=' . + \ get(a:config, 'configPrecedence', g:prettier#config#config_precedence) . + \ ' --prose-wrap=' . + \ get(a:config, 'proseWrap', g:prettier#config#prose_wrap) . + \ ' --html-whitespace-sensitivity ' . + \ get(a:config, 'htmlWhitespaceSensitivity', g:prettier#config#html_whitespace_sensitivity) . + \ ' --stdin-filepath="'.simplify(expand('%:p')).'"' . + \ ' --require-pragma=' . + \ get(a:config, 'requirePragma', g:prettier#config#require_pragma) . + \ ' --loglevel error '. + \ ' --stdin ' + return l:cmd +endfunction + +" Returns either '--range-start X --range-end Y' or an empty string. +function! s:Flag_range_delimiter(config, partialFormatEnabled, start, end) abort + if (!a:partialFormatEnabled) + return '' + endif + + let l:range = prettier#utils#buffer#getCharRange(a:start, a:end) + + return '--range-start=' . l:range[0] . ' --range-end=' . l:range[1] +endfunction + +" Returns '--tab-width=NN' +function! s:Flag_tab_width(config) abort + let l:value = get(a:config, 'tabWidth', g:prettier#config#tab_width) + + if (l:value ==# 'auto') + let l:value = prettier#utils#shim#shiftwidth() + endif + + return '--tab-width=' . l:value +endfunction + +" Returns either '--use-tabs' or an empty string. +function! s:Flag_use_tabs(config) abort + let l:value = get(a:config, 'useTabs', g:prettier#config#use_tabs) + if (l:value ==# 'auto') + let l:value = &expandtab ? 'false' : 'true' + endif + + if ( l:value ==# 'true' ) + return ' --use-tabs' + else + return '' + endif +endfunction + +" Returns '--print-width=NN' or '' +function! s:Flag_print_width(config) abort + let l:value = get(a:config, 'printWidth', g:prettier#config#print_width) + + if (l:value ==# 'auto') + let l:value = &textwidth + endif + + if (l:value > 0) + return '--print-width=' . l:value + else + return '' + endif +endfunction + +" Returns '--parser=PARSER' or '' +function! s:Flag_parser(config) abort + let l:value = get(a:config, 'parser', g:prettier#config#parser) + + if (l:value !=# '') + return '--parser=' . l:value + else + return '' + endif +endfunction diff --git a/autoload/prettier/resolver/executable.vim b/autoload/prettier/resolver/executable.vim new file mode 100644 index 0000000..8d233ab --- /dev/null +++ b/autoload/prettier/resolver/executable.vim @@ -0,0 +1,74 @@ +let s:ROOT_DIR = fnamemodify(resolve(expand('<sfile>:p')), ':h') + +" By default we will search for the following +" => user defined prettier cli path from vim configuration file +" => locally installed prettier inside node_modules on any parent folder +" => globally installed prettier +" => vim-prettier prettier installation +" => if all fails suggest install +function! prettier#resolver#executable#getPath() abort + let l:user_defined_exec_path = fnamemodify(g:prettier#exec_cmd_path, ':p') + if executable(l:user_defined_exec_path) + return l:user_defined_exec_path + endif + + let l:localExec = s:ResolveExecutable(getcwd()) + if executable(l:localExec) + return fnameescape(l:localExec) + endif + + let l:globalExec = s:ResolveExecutable() + if executable(l:globalExec) + return fnameescape(l:globalExec) + endif + + let l:pluginExec = s:ResolveExecutable(s:ROOT_DIR) + if executable(l:pluginExec) + return fnameescape(l:pluginExec) + endif + + return -1 +endfunction + +function! s:GetExecPath(...) abort + let l:rootDir = a:0 > 0 ? a:1 : -1 + let l:dir = l:rootDir != -1 ? l:rootDir . '/.bin/' : '' + return l:dir . 'prettier' +endfunction + +" Searches for the existence of a directory accross +" ancestral parents +function! s:TraverseAncestorDirSearch(rootDir) abort + let l:root = a:rootDir + let l:dir = 'node_modules' + + while 1 + let l:searchDir = l:root . '/' . l:dir + if isdirectory(l:searchDir) + return l:searchDir + endif + + let l:parent = fnamemodify(l:root, ':h') + if l:parent == l:root + return -1 + endif + + let l:root = l:parent + endwhile +endfunction + +function! s:ResolveExecutable(...) abort + let l:rootDir = a:0 > 0 ? a:1 : 0 + let l:exec = -1 + + if isdirectory(l:rootDir) + let l:dir = s:TraverseAncestorDirSearch(l:rootDir) + if l:dir != -1 + let l:exec = s:GetExecPath(l:dir) + endif + else + let l:exec = s:GetExecPath() + endif + + return l:exec +endfunction diff --git a/autoload/prettier/resolver/preset.vim b/autoload/prettier/resolver/preset.vim new file mode 100644 index 0000000..03f98ee --- /dev/null +++ b/autoload/prettier/resolver/preset.vim @@ -0,0 +1,8 @@ +" Build config using predefined preset +function! prettier#resolver#preset#resolve(fileTypeConfigOverwrites) abort + if ( g:prettier#preset#config ==# 'fb' ) + return extend(prettier#presets#fb#config(), a:fileTypeConfigOverwrites) + endif + + return a:fileTypeConfigOverwrites +endfunction diff --git a/autoload/prettier/utils/buffer.vim b/autoload/prettier/utils/buffer.vim new file mode 100644 index 0000000..7bc7ab1 --- /dev/null +++ b/autoload/prettier/utils/buffer.vim @@ -0,0 +1,70 @@ +function! prettier#utils#buffer#replace(lines, startSelection, endSelection) abort + " store view + let l:winview = winsaveview() + let l:newBuffer = prettier#utils#buffer#createBufferFromUpdatedLines(a:lines, a:startSelection, a:endSelection) + + " we should not replace contents if the newBuffer is empty + if empty(l:newBuffer) + return + endif + + " https://vim.fandom.com/wiki/Restore_the_cursor_position_after_undoing_text_change_made_by_a_script + " create a fake change entry and merge with undo stack prior to do formating + normal! ix + normal! x + try | silent undojoin | catch | endtry + + " delete all lines on the current buffer + silent! execute '%delete _' + + " replace all lines from the current buffer with output from prettier + let l:idx = 0 + for l:line in l:newBuffer + silent! call append(l:idx, l:line) + let l:idx += 1 + endfor + + " delete trailing newline introduced by the above append procedure + silent! execute '$delete _' + + " Restore view + call winrestview(l:winview) + +endfunction + +" Replace and save the buffer +function! prettier#utils#buffer#replaceAndSave(lines, startSelection, endSelection) abort + call prettier#utils#buffer#replace(a:lines, a:startSelection, a:endSelection) + noautocmd write +endfunction + +" Returns 1 if content has changed +function! prettier#utils#buffer#willUpdatedLinesChangeBuffer(lines, start, end) abort + return getbufline(bufnr('%'), 1, line('$')) == prettier#utils#buffer#createBufferFromUpdatedLines(a:lines, a:start, a:end) ? 0 : 1 +endfunction + +" Returns a new buffer with lines replacing start and end of the contents of the current buffer +function! prettier#utils#buffer#createBufferFromUpdatedLines(lines, start, end) abort + return getbufline(bufnr('%'), 1, a:start - 1) + a:lines + getbufline(bufnr('%'), a:end + 1, '$') +endfunction + +" Adapted from https://github.com/farazdagi/vim-go-ide +function! s:getCharPosition(line, col) abort + if &encoding !=# 'utf-8' + " On utf-8 enconding we can't just use bytes so we need to make sure we can count the + " characters, we do that by adding the text into a temporary buffer and counting the chars + let l:buf = a:line == 1 ? '' : (join(getline(1, a:line - 1), "\n") . "\n") + let l:buf .= a:col == 1 ? '' : getline('.')[:a:col - 2] + return len(iconv(l:buf, &encoding, 'utf-8')) + endif + " On non utf-8 the line byte should match the character + return line2byte(a:line) + (a:col - 2) +endfun + +" Returns [start, end] byte range when on visual mode +function! prettier#utils#buffer#getCharRange(startSelection, endSelection) abort + let l:range = [] + call add(l:range, s:getCharPosition(a:startSelection, col("'<"))) + call add(l:range, s:getCharPosition(a:endSelection, col("'>"))) + return l:range +endfunction diff --git a/autoload/prettier/utils/quickfix.vim b/autoload/prettier/utils/quickfix.vim new file mode 100644 index 0000000..b4766a7 --- /dev/null +++ b/autoload/prettier/utils/quickfix.vim @@ -0,0 +1,22 @@ +" We use this flag so that we ensure only clearing quickfix if it was created by prettier itself +let s:prettier_quickfix_open = 0 + +function! prettier#utils#quickfix#close() abort + " close quickfix if it is opened + if s:prettier_quickfix_open + call setqflist([], 'r') + cclose + let s:prettier_quickfix_open = 0 + endif +endfunction + +function! prettier#utils#quickfix#open(errors, focus) abort + let s:prettier_quickfix_open = 1 + let l:winnr = winnr() + call setqflist(a:errors, 'r') + botright copen + if !a:focus + " Return the cursor back to the main buffer. + exe l:winnr . 'wincmd w' + endif +endfunction diff --git a/autoload/prettier/utils/shim.vim b/autoload/prettier/utils/shim.vim new file mode 100644 index 0000000..228829e --- /dev/null +++ b/autoload/prettier/utils/shim.vim @@ -0,0 +1,8 @@ +" Backwards compatable version of shiftwidth() +function! prettier#utils#shim#shiftwidth() abort + if exists('*shiftwidth') + return shiftwidth() + else + return &shiftwidth + endif +endfunction |
