Skip to content

Commit

Permalink
plugin: formatter: Add plugin to format files
Browse files Browse the repository at this point in the history
  • Loading branch information
taconi committed Mar 7, 2024
1 parent c15abea commit f6d45a3
Show file tree
Hide file tree
Showing 4 changed files with 612 additions and 0 deletions.
3 changes: 3 additions & 0 deletions runtime/help/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ or disable them:
* `diff`: integrates the `diffgutter` option with Git. If you are in a Git
directory, the diff gutter will show changes with respect to the most
recent Git commit rather than the diff since opening the file.
* `formatter`: Formatter settings can be for any type of file (javascript,
go, python).

Any option you set in the editor will be saved to the file
~/.config/micro/settings.json so, in effect, your configuration file will be
Expand Down Expand Up @@ -484,6 +486,7 @@ so that you can see what the formatting should look like.
"fastdirty": false,
"fileformat": "unix",
"filetype": "unknown",
"formatter": true,
"incsearch": true,
"ftoptions": true,
"ignorecase": true,
Expand Down
2 changes: 2 additions & 0 deletions runtime/help/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ There are 6 default plugins that come pre-installed with micro. These are
* `diff`: integrates the `diffgutter` option with Git. If you are in a Git
directory, the diff gutter will show changes with respect to the most
recent Git commit rather than the diff since opening the file.
* `formatter`: Formatter settings can be for any type of file (javascript,
go, python).

See `> help linter`, `> help comment`, and `> help status` for additional
documentation specific to those plugins.
Expand Down
278 changes: 278 additions & 0 deletions runtime/plugins/formatter/formatter.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
VERSION = '1.0.0'

local micro = import('micro')
local config = import('micro/config')
local shell = import('micro/shell')

local errors = import('errors')
local fmt = import('fmt')
local regexp = import('regexp')
local runtime = import('runtime')
local strings = import('strings')

---@alias Error { Error: fun(): string } # Goland error

---@class Buffer
---@field Path string
---@field AbsPath string
---@field FileType fun(): string
---@field ReOpen fun()

---@class BufPane userdata # Micro BufPane
---@field Buf Buffer

-- luacheck: globals toString
---@param value any
function toString(value)
if type(value) == 'string' then
return value
elseif type(value) == 'table' then
return strings.Join(value, ' ')
end
return value
end

-- luacheck: globals contains
---@param t table # Table to check.
---@param e any # Element to verify.
---@return boolean # If contains or not
function contains(t, e)
for i = 1, #t do
if t[i] == e then
return true
end
end
return false
end

-- luacheck: globals Format
---@class Format
---@field cmd string
---@field args string|string[]
---@field name string
---@field bind string
---@field onSave boolean
---@field filetypes string[]
---@field os string[]
---@field whitelist boolean
---@field domatch boolean
---@field callback fun(buf: Buffer): boolean
local Format = {}

---validates a formatter
---@param format Format
---@return Format?, Error?
---@nodiscard
function Format:new(format)
---@type Format
local f = {}

if format.cmd == nil or type(format.cmd) ~= 'string' then
return format, errors.New('Invalid "cmd"')
elseif format.filetypes == nil or type(format.filetypes) ~= 'table' then
return format, errors.New('Invalid "filetypes"')
end

f.cmd = format.cmd
f.filetypes = format.filetypes

---@type string[]
local cmds

if not format.name then
cmds = strings.Split(format.cmd, ' ')
f.name = fmt.Sprintf('%s', cmds[1])
else
f.name = format.name
end

f.bind = format.bind
f.args = toString(format.args) or ''
f.onSave = format.onSave
f.os = format.os
f.whitelist = format.whitelist or false
f.domatch = format.domatch or false
f.callback = format.callback

self.__index = self
return setmetatable(f, self), nil
end

function Format:hasOS()
if self.os == nil then
return true
end
local has_os = contains(self.os, runtime.GOOS)
if (not has_os and self.whitelist) or (has_os and not self.whitelist) then
return false
end

return true
end

---@param buf Buffer
---@param filter fun(f: Format): boolean
---@return boolean
function Format:hasFormat(buf, filter)
if filter ~= nil and not filter(self) then
return false
end

---@type string
local filetype = buf:FileType()
---@type string[]
local filetypes = self.filetypes

for _, ft in ipairs(filetypes) do
if self.domatch then
if regexp.MatchString(ft, buf.AbsPath) then
return true
end
elseif ft == filetype then
return true
end
end
return false
end

function Format:hasCallback(buf)
if self.callback ~= nil and type(self.callback) == 'function' and not self.callback(buf) then
return false
end
return true
end

---run a formatter on a given file
---@param buf Buffer
---@return Error?
function Format:run(buf)
---@type string
local args = self.args:gsub('%%f', buf.Path)
---@type string
local cmd = fmt.Sprintf('%s %s', self.cmd:gsub('%%f', buf.Path), args)
-- err: Error?
local _, err = shell.RunCommand(cmd)

---@type string
if err ~= nil then
return err
end
end

---@type Format[]
-- luacheck: globals formatters
formatters = {}

-- luacheck: globals format
---format a bufpane
---@param bp BufPane
---@param filter? fun(f: Format): boolean
---@return Error?
function format(bp, filter)
if #formatters < 1 then
return
end

---@type string
local errs = ''
for _, format in ipairs(formatters) do
---@cast filter fun(f: Format): boolean
if format:hasFormat(bp.Buf, filter) and format:hasOS() and format:hasCallback(bp.Buf) then
local err = format:run(bp.Buf)
if err ~= nil then
errs = fmt.Sprintf('%s | %s', errs, format.name)
end
end
end

bp.Buf:ReOpen()

if errs ~= '' then
return micro.InfoBar():Error('💥 Error when using formatters: %s', errs)
else
micro.InfoBar():Message(fmt.Sprintf('🎬 File formatted successfully! %s ✨ 🍰 ✨', bp.Buf.Path))
end
end

-- luacheck: globals makeCommand
---creates a formatter command (if the name is not specified, all found formatters will be executed)
---@param name string # Format name
---@return fun(bp: BufPane)
function makeCommand(name)
---@param bp BufPane
return function(bp)
return format(bp, function(format)
return name == format.name
end)
end
end

-- luacheck: globals makeFormat
---add a format
---@param format Format
---@return Error?
function makeFormat(format)
local err
-- format: Format
-- err: Error?
format, err = Format:new(format)
if err ~= nil then
return err
end
table.insert(formatters, format)

---@type boolean
local makeCommands = config.GetGlobalOption('formatter.makeCommands')

if makeCommands then
config.MakeCommand(format.name, makeCommand(format.name), config.NoComplete)
end

if format.bind then
if not makeCommands then
config.MakeCommand(format.name, makeCommand(format.name), config.NoComplete)
end

config.TryBindKey(format.bind, 'command:' .. format.name, true)
end
end

-- luacheck: globals setup
---initialize formatters
---@param formats Format[]
function setup(formats)
---@type string
for _, format in ipairs(formats) do
---@type Error?
makeFormat(format)
end
end

-- CALLBACK'S

---runs formatters set to onSave
---@param bp BufPane
function onSave(bp)
if #formatters < 1 then
return true
end

---@type Error?
local err = format(bp, function(format)
return format.onSave == true
end)

if err ~= nil then
micro.InfoBar():Error(fmt.Sprintf('%v', err))
else
micro.InfoBar():Message(fmt.Sprintf('🎬 Saved! %s ✨ 🍰 ✨', bp.Buf.Path))
end
return true
end

function init()
config.RegisterCommonOption('formatter', 'makeCommands', false)
config.AddRuntimeFile('formatter', config.RTHelp, 'help/formatter.md')
config.MakeCommand('format', format, config.NoComplete)
config.TryBindKey('Alt-f', 'command:format', false)
end
Loading

0 comments on commit f6d45a3

Please sign in to comment.