summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Vink <59492084+ivi-vink@users.noreply.github.com>2025-01-29 12:44:07 +0000
committerMike Vink <59492084+ivi-vink@users.noreply.github.com>2025-01-29 12:44:07 +0000
commite21a4dc7616d8c871f0a14c0ddd58b0b60fe4821 (patch)
tree735ddad8867f6ab12d9b242cf4bfee7f3eba08e3
Squashed 'mut/vis/vis-format/' content from commit 3a6a578
git-subtree-dir: mut/vis/vis-format git-subtree-split: 3a6a578e340c87a57aade4ba5c88388fca11e586
-rw-r--r--.editorconfig10
-rw-r--r--LICENSE121
-rw-r--r--README.md153
-rw-r--r--init.lua224
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/LICENSE
@@ -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,
+}