diff options
| -rw-r--r-- | .editorconfig | 10 | ||||
| -rw-r--r-- | LICENSE | 121 | ||||
| -rw-r--r-- | README.md | 153 | ||||
| -rw-r--r-- | init.lua | 224 |
4 files changed, 508 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7c67854 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +[*.lua] +indent_style = space +indent_size = 2 +max_line_length = 79 +quote_type = single + +[*.md] +max_line_length = 79 +indent_style = space +indent_size = 2 @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4435542 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# vis-format - integrates vis with external formatters + +A plugin for [vis](https://github.com/martanne/vis) to integrate `prettier`, +`rustfmt` etc. + +### Installation + +Clone this repository to where you install your plugins. (If this is your first +plugin, running `git clone https://github.com/milhnl/vis-format` in +`~/.config/vis/` will probably work). + +Then, add something like the following to your `visrc`. + + local format = require('vis-format') + vis:map(vis.modes.NORMAL, '=', format.apply) + +### Usage + +Press `=` to format the whole file. + +Currently, the following formatters are supported out-of-the-box. + +- `bash`: [shfmt](https://github.com/mvdan/sh) +- `csharp`: [CSharpier](https://csharpier.com/). Although + [dotnet-format](https://github.com/dotnet/format) is the 'default' formatter + for dotnet, it does not support formatting stdin (and does not break lines). +- `git-commit`: Actually `diff` with an extra check, as that's how + `vis.ftdetect` ends up labeling it. Uses `fmt -w` and some glue code to + reformat the commit body, but not the summary and comments. +- `go`: `gofmt` +- `lua`: [StyLua](https://github.com/JohnnyMorganz/StyLua) and + [LuaFormatter](https://github.com/Koihik/LuaFormatter), depending on which + config file is in the working directory. +- `markdown`: `prettier` with `--prose-wrap` enabled if `colorcolumn` is set. +- `powershell`: + [PSScriptAnalyzer](https://learn.microsoft.com/en-gb/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules#installing-psscriptanalyzer) + via `powershell.exe` in the WSL if available or `pwsh`. +- `rust`: `rustfmt` +- `text`: `fmt` with width based on colorcolumn if set, otherwise joined + paragraphs. + +I'm working on some more heuristics for detecting which formatter to use for +languages without a 'blessed' formatter. In the meantime, this is how you add +the ones you want to use: + + format.formatters.html = format.stdio_formatter("prettier --parser html") + +### Advanced usage + +The following methods and tables are fields of the table that is returned by +`require('vis-format')` (e.g. `format`). You can use them to extend or +configure `vis-format`. + +#### `formatters` + +A table containing the configured formatters. There are some predefined (see +the list above under "Usage"), and you can add or override those by assigning +your formatter to a key corresponding to the vis `syntax` it is relevant for. +Each entry is of either of these forms: + + { + apply = func(win, range, pos) end, + options = { + ranged = nil, + check_same = nil, + } + } + +or + + { + pick = func(win, range, pos) end, + } + +The first form directly defines a formatter. An entry containing `pick` is +executed and its result is directly used as a formatter. A recommended way to +use `pick` is to return a different formatter from the `formatters` table. Note +that using the vis `syntax` as keys in this table is only necessary for +formatters that are run directly. This means that `lua` is a good key for a +`pick` entry, which chooses between the formatters found at `stylua` and +`luaformatter`. + +#### `stdio_formatter` + +The `stdio_formatter` function wraps the command to produce something like +this: + + { + apply = function(win, range, pos) end, + options = { ranged = false } + } + +The command given can also be a function, which is expected to return a string, +which is then used as a command. This allows you to use the given range, or +options from `win`. `ranged` is automatically set to true in this case. + +Apart from mapping `vis-format` in normal mode, you can also define an operator +(with `vis:operator_new`) to format ranges of code/text. This will require a +formatter that can work with ranges of text. Configuring that looks like this: + + -- Add a formatter that can use a range + format.formatters.lua = format.stdio_formatter(function(win, range, pos) + return 'stylua -s --range-start ' .. range.start .. ' --range-end ' + .. range.finish .. ' -' + end) + + -- Bind it to keys + vis:operator_new('=', format.apply) -- this'll handle ranges + vis:map(vis.modes.NORMAL, '=', function() -- this'll format whole files + format.apply(vis.win.file, nil, vis.win.selection.pos) + end) + +#### `with_filename` + +Most formatters take a path for where `stdin` would be. If the file has a path +`with_filename` concatenates the option and the shell-escaped path. Note that +it does not add any spaces to separate options/arguments. + + stdio_formatter(function(win) + return 'shfmt ' .. with_filename(win, '--filename ') .. ' -' + end, { ranged = false }) + +#### `options` + +- `options.check_same` (`boolean|number`) — After formatting, to avoid updating + the file, `vis-format` can compare the old and the new. If this is set to a + number, that's the maximum size of the file for which it is enabled. This + option is also available in the per-formatter options. + +### Bugs + +Ranged formatting is not enabled and will currently not work with `prettier`. +Prettier extends the range given on the command line to the beginning and end +of the statement containing it. This will not work with how `vis-format` +currently applies the output. I have some ideas on how to fix this, but wanted +to release what works first. + +#### Note on editor options and vis versions before 0.9 + +`vis` has an `options` table with editor settings like tab width etc. built-in +since 0.9. `vis-format` will not read this most of the time, because the +correct way to configure formatters is with their dedicated configuration +files, or `.editorconfig` (Which can be read in vis with +[vis-editorconfig](https://github.com/seifferth/vis-editorconfig) and +[vis-editorconfig-options](https://github.com/milhnl/vis-editorconfig-options) +). + +The included formatter integrations that _do_ use this information assume that +the `options` table in `vis` and `win` is present. If you use an older version, +`vis-format` will still work, but can't detect your editor settings. To fix +that, look at the +[vis-options-backport](https://github.com/milhnl/vis-options-backport) plugin. +This will 'polyfill' that for older versions. diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..4b1183b --- /dev/null +++ b/init.lua @@ -0,0 +1,224 @@ +local global_options = { + check_same = true, +} + +local func_formatter = function(func, options) + local apply = function(win, range, pos) + local size = win.file.size + local all = { start = 0, finish = size } + if range == nil then + range = all + end + local check_same = (options and options.check_same ~= nil) + and options.check_same + or global_options.check_same + local check = check_same == true + or (type(check_same) == 'number' and check_same >= size) + out, err = func(win, range, pos) + if err ~= nil then + if err:match('\n') then + vis:message(err) + else + vis:info(err) + end + return + elseif out == nil or out == '' then + vis:info('No output from formatter') + elseif not check or win.file:content(all) ~= out then + local start, finish = range.start, range.finish + win.file:delete(range) + win.file:insert(start, out:sub(start + 1, finish + (out:len() - size))) + end + return pos + end + return { + apply = apply, + options = options, + } +end + +local stdio_formatter = function(cmd, options) + return func_formatter(function(win, range, pos) + local command = type(cmd) == 'function' and cmd(win, range, pos) or cmd + local status, out, err = vis:pipe(win.file, range, command) + if status ~= 0 then + return nil, err + end + return out, nil + end, options or { ranged = type(cmd) == 'function' }) +end + +local with_filename = function(win, option) + if win.file.path then + return option .. "'" .. win.file.path:gsub("'", "\\'") .. "'" + else + return '' + end +end + +local formatters = {} +formatters = { + bash = stdio_formatter(function(win) + return 'shfmt ' .. with_filename(win, '--filename ') .. ' -' + end), + csharp = stdio_formatter('dotnet csharpier'), + diff = { + pick = function(win) + for _, pattern in ipairs(vis.ftdetect.filetypes['git-commit'].ext) do + if ((win.file.name or ''):match('[^/]+$') or ''):match(pattern) then + return formatters['git-commit'] + end + end + end, + }, + ['git-commit'] = func_formatter(function(win, range, pos) + local width = (win.options and win.options.colorcolumn ~= 0) + and (win.options.colorcolumn - 1) + or 72 + local parts = {} + local fmt = nil + local summary = true + for line in win.file:lines_iterator() do + local txt = not line:match('^#') + if fmt == nil or fmt ~= txt then + fmt = txt and not summary + local prev = parts[#parts] and parts[#parts].finish or 0 + parts[#parts + 1] = { + fmt = fmt, + start = prev, + finish = prev + #line + 1, + } + summary = summary and not txt + else + parts[#parts].finish = parts[#parts].finish + #line + 1 + end + end + local out = '' + for _, part in ipairs(parts) do + if part.fmt then + local status, partout, err = + vis:pipe(win.file, part, 'fmt -w ' .. width) + if status ~= 0 then + return nil, err + end + out = out .. (partout or '') + else + out = out .. win.file:content(part) + end + end + return out + end, { ranged = false }), + go = stdio_formatter('gofmt'), + lua = { + pick = function(win) + local fz = io.popen([[ + test -e .lua-format && echo luaformatter || echo stylua + ]]) + if fz then + local out = fz:read('*a') + local _, _, status = fz:close() + if status == 0 then + return formatters[out:gsub('\n$', '')] + end + end + end, + }, + luaformatter = stdio_formatter('lua-format'), + markdown = stdio_formatter(function(win) + if win.options and win.options.colorcolumn ~= 0 then + return 'prettier --parser markdown --prose-wrap always ' + .. ('--print-width ' .. (win.options.colorcolumn - 1) .. ' ') + .. with_filename(win, '--stdin-filepath ') + else + return 'prettier --parser markdown ' + .. with_filename(win, '--stdin-filepath ') + end + end, { ranged = false }), + powershell = stdio_formatter([[ + "$( (command -v powershell.exe || command -v pwsh) 2>/dev/null )" -c ' + Invoke-Formatter -ScriptDefinition ` + ([IO.StreamReader]::new([Console]::OpenStandardInput()).ReadToEnd()) + ' | sed -e :a -e '/^[\r\n]*$/{$d;N;};/\n$/ba' + ]]), + rust = stdio_formatter('rustfmt'), + stylua = stdio_formatter(function(win, range) + if range and (range.start ~= 0 or range.finish ~= win.file.size) then + return 'stylua -s --range-start ' + .. range.start + .. ' --range-end ' + .. range.finish + .. with_filename(win, ' --stdin-filepath ') + .. ' -' + else + return 'stylua -s ' .. with_filename(win, '--stdin-filepath ') .. ' -' + end + end), + text = stdio_formatter(function(win) + if win.options and win.options.colorcolumn ~= 0 then + return 'fmt -w ' .. (win.options.colorcolumn - 1) + else + return "fmt | awk -v n=-1 '" + .. ' {' + .. ' if ($0 == "") {' + .. ' n = n <= 0 ? 2 : 1' + .. ' } else {' + .. ' if (n == 0) sub(/^ */, "");' + .. ' n = 0;' + .. ' }' + .. ' printf("%s", $0 (n == 0 ? " " : ""));' + .. ' for(i = 0; i < n; i++)' + .. ' printf("\\n");' + .. ' }' + .. "'" + end + end, { ranged = false }), +} + +local getwinforfile = function(file) + for win in vis:windows() do + if win and win.file and win.file.path == file.path then + return win + end + end +end + +local apply = function(file_or_keys, range, pos) + local win = type(file_or_keys) ~= 'string' and getwinforfile(file_or_keys) + or vis.win + local ret = type(file_or_keys) ~= 'string' + and function() + return pos + end + or function() + return 0 + end + pos = pos or win.selection.pos + local formatter = formatters[win.syntax] + if formatter and formatter.pick then + formatter = formatter.pick(win) + end + if formatter == nil then + vis:info('No formatter for ' .. win.syntax) + return ret() + end + if + range ~= nil + and not formatter.options.ranged + and range.start ~= 0 + and range.finish ~= win.file.size + then + vis:info('Formatter for ' .. win.syntax .. ' does not support ranges') + return ret() + end + pos = formatter.apply(win, range, pos) or pos + vis:insert('') -- redraw and friends don't work + return ret() +end + +return { + formatters = formatters, + options = globalOptions, + apply = apply, + stdio_formatter = stdio_formatter, + with_filename = with_filename, +} |
