Add linter plugin support

This commit is contained in:
Zachary Yedidia
2019-08-02 23:46:25 -07:00
parent e7e0272968
commit 4027081e0e
11 changed files with 166 additions and 99 deletions

View File

@@ -80,7 +80,7 @@ func luaImportMicroUtil() *lua.LTable {
ulua.L.SetField(pkg, "RuneAt", luar.New(ulua.L, util.LuaRuneAt))
ulua.L.SetField(pkg, "GetLeadingWhitespace", luar.New(ulua.L, util.LuaGetLeadingWhitespace))
ulua.L.SetField(pkg, "", luar.New(ulua.L, util.LuaIsWordChar))
ulua.L.SetField(pkg, "IsWordChar", luar.New(ulua.L, util.LuaIsWordChar))
return pkg
}

View File

@@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"sort"
@@ -172,7 +173,10 @@ func main() {
screen.TermMessage(err)
}
config.LoadAllPlugins()
err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
}
err = config.RunPluginFn("init")
if err != nil {
screen.TermMessage(err)
@@ -234,6 +238,7 @@ func main() {
select {
case f := <-shell.Jobs:
// If a new job has finished while running in the background we should execute the callback
log.Println("OUTPUT:", f.Output)
f.Function(f.Output, f.Args...)
case event = <-events:
case <-screen.DrawChan:

View File

@@ -1,6 +1,7 @@
package action
import (
"log"
"strings"
"time"
@@ -261,7 +262,8 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
// canceled by plugin
continue
}
if action(h) && h.PluginCB("on"+estr) {
rel := action(h)
if h.PluginCB("on"+estr) && rel {
h.Relocate()
}
}
@@ -271,7 +273,9 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
if !h.PluginCB("pre" + estr) {
return false
}
if action(h) && h.PluginCB("on"+estr) {
rel := action(h)
log.Println("calling on", estr)
if h.PluginCB("on"+estr) && rel {
h.Relocate()
}
return true

View File

@@ -367,8 +367,13 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error {
}
} else {
for _, pl := range config.Plugins {
if option == pl.Name && nativeValue.(bool) && !pl.Loaded {
pl.Load()
if option == pl.Name {
if nativeValue.(bool) && !pl.Loaded {
pl.Load()
pl.Call("init")
} else if !nativeValue.(bool) && pl.Loaded {
pl.Call("deinit")
}
}
}
}

View File

@@ -17,10 +17,10 @@ type Message struct {
Msg string
Start, End Loc
Kind MsgType
Owner int
Owner string
}
func NewMessage(owner int, msg string, start, end Loc, kind MsgType) *Message {
func NewMessage(owner string, msg string, start, end Loc, kind MsgType) *Message {
return &Message{
Msg: msg,
Start: start,
@@ -30,8 +30,8 @@ func NewMessage(owner int, msg string, start, end Loc, kind MsgType) *Message {
}
}
func NewMessageAtLine(owner int, msg string, line int, kind MsgType) *Message {
start := Loc{-1, line}
func NewMessageAtLine(owner string, msg string, line int, kind MsgType) *Message {
start := Loc{-1, line - 1}
end := start
return NewMessage(owner, msg, start, end, kind)
}
@@ -64,7 +64,7 @@ func (b *Buffer) removeMsg(i int) {
b.Messages = b.Messages[:len(b.Messages)-1]
}
func (b *Buffer) ClearMessages(owner int) {
func (b *Buffer) ClearMessages(owner string) {
for i := len(b.Messages) - 1; i >= 0; i-- {
if b.Messages[i].Owner == owner {
b.removeMsg(i)

View File

@@ -2,6 +2,7 @@ package config
import (
"errors"
"log"
lua "github.com/yuin/gopher-lua"
ulua "github.com/zyedidia/micro/internal/lua"
@@ -10,10 +11,15 @@ import (
var ErrNoSuchFunction = errors.New("No such function exists")
// LoadAllPlugins loads all detected plugins (in runtime/plugins and ConfigDir/plugins)
func LoadAllPlugins() {
func LoadAllPlugins() error {
var reterr error
for _, p := range Plugins {
p.Load()
err := p.Load()
if err != nil {
reterr = err
}
}
return reterr
}
// RunPluginFn runs a given function in all plugins
@@ -68,7 +74,7 @@ type Plugin struct {
func (p *Plugin) IsEnabled() bool {
if v, ok := GlobalSettings[p.Name]; ok {
return v.(bool)
return v.(bool) && p.Loaded
}
return true
}
@@ -77,7 +83,7 @@ var Plugins []*Plugin
func (p *Plugin) Load() error {
for _, f := range p.Srcs {
if !p.IsEnabled() {
if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) {
return nil
}
dat, err := f.Data()
@@ -98,6 +104,7 @@ func (p *Plugin) Load() error {
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
plug := ulua.L.GetGlobal(p.Name)
log.Println(p.Name, fn, plug)
luafn := ulua.L.GetField(plug, fn)
if luafn == lua.LNil {
return nil, ErrNoSuchFunction

File diff suppressed because one or more lines are too long

View File

@@ -57,7 +57,7 @@ func Import(pkg string) *lua.LTable {
return importRuntime()
case "path":
return importPath()
case "filepath":
case "path/filepath", "filepath":
return importFilePath()
case "strings":
return importStrings()
@@ -67,7 +67,7 @@ func Import(pkg string) *lua.LTable {
return importErrors()
case "time":
return importTime()
case "utf8":
case "unicode/utf8", "utf8":
return importUtf8()
default:
return nil

View File

@@ -32,17 +32,17 @@ func init() {
// JobFunction is a representation of a job (this data structure is what is loaded
// into the jobs channel)
type JobFunction struct {
Function func(string, ...string)
Function func(string, ...interface{})
Output string
Args []string
Args []interface{}
}
// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events
type CallbackFile struct {
io.Writer
callback func(string, ...string)
args []string
callback func(string, ...interface{})
args []interface{}
}
func (f *CallbackFile) Write(data []byte) (int, error) {
@@ -55,13 +55,13 @@ func (f *CallbackFile) Write(data []byte) (int, error) {
// JobStart starts a shell command in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...interface{}) *exec.Cmd {
return JobSpawn("sh", []string{"-c", cmd}, onStdout, onStderr, onExit, userargs...)
}
// JobSpawn starts a process with args in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit string, userargs ...interface{}) *exec.Cmd {
// Set up everything correctly if the functions have been provided
proc := exec.Command(cmdName, cmdArgs...)
var outbuf bytes.Buffer
@@ -104,12 +104,13 @@ func JobSend(cmd *exec.Cmd, data string) {
// luaFunctionJob returns a function that will call the given lua function
// structured as a job call i.e. the job output and arguments are provided
// to the lua function
func luaFunctionJob(fn string) func(string, ...string) {
func luaFunctionJob(fn string) func(string, ...interface{}) {
luaFn := strings.Split(fn, ".")
plName, plFn := luaFn[0], luaFn[1]
pl := config.FindPlugin(plName)
return func(output string, args ...string) {
return func(output string, args ...interface{}) {
var luaArgs []lua.LValue
luaArgs = append(luaArgs, luar.New(ulua.L, output))
for _, v := range args {
luaArgs = append(luaArgs, luar.New(ulua.L, v))
}
@@ -119,11 +120,3 @@ func luaFunctionJob(fn string) func(string, ...string) {
}
}
}
func unpack(old []string) []interface{} {
new := make([]interface{}, len(old))
for i, v := range old {
new[i] = v
}
return new
}

View File

@@ -3,6 +3,7 @@ package util
import (
"errors"
"fmt"
"log"
"os"
"os/user"
"path/filepath"
@@ -202,10 +203,10 @@ func FSize(f *os.File) int64 {
}
// IsWordChar returns whether or not the string is a 'word character'
// If it is a unicode character, then it does not match
// Word characters are defined as [A-Za-z0-9_]
// Word characters are defined as numbers, letters, or '_'
func IsWordChar(r rune) bool {
return (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r == '_')
log.Println("IsWordChar")
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
}
// Spaces returns a string with n spaces
@@ -237,7 +238,7 @@ func IsSpacesOrTabs(str []byte) bool {
// IsWhitespace returns true if the given rune is a space, tab, or newline
func IsWhitespace(c rune) bool {
return c == ' ' || c == '\t' || c == '\n'
return unicode.IsSpace(c)
}
// IsBytesWhitespace returns true if the given bytes are all whitespace

View File

@@ -1,75 +1,126 @@
if GetOption("linter") == nil then
AddOption("linter", true)
local runtime = import("runtime")
local filepath = import("path/filepath")
local shell = import("micro/shell")
local buffer = import("micro/buffer")
local micro = import("micro")
local linters = {}
-- creates a linter entry, call from within an initialization function, not
-- directly at initial load time
--
-- name: name of the linter
-- filetype: filetype to check for to use linter
-- cmd: main linter process that is executed
-- args: arguments to pass to the linter process
-- use %f to refer to the current file name
-- use %d to refer to the current directory name
-- errorformat: how to parse the linter/compiler process output
-- %f: file, %l: line number, %m: error/warning message
-- os: list of OSs this linter is supported or unsupported on
-- optional param, default: []
-- whitelist: should the OS list be a blacklist (do not run the linter for these OSs)
-- or a whitelist (only run the linter for these OSs)
-- optional param, default: false (should blacklist)
-- domatch: should the filetype be interpreted as a lua pattern to match with
-- the actual filetype, or should the linter only activate on an exact match
-- optional param, default: false (require exact match)
function makeLinter(name, filetype, cmd, args, errorformat, os, whitelist, domatch)
if linters[name] == nil then
linters[name] = {}
linters[name].filetype = filetype
linters[name].cmd = cmd
linters[name].args = args
linters[name].errorformat = errorformat
linters[name].os = os or {}
linters[name].whitelist = whitelist or false
linters[name].domatch = domatch or false
end
end
MakeCommand("lint", "linter.lintCommand", 0)
function lintCommand()
CurView():Save(false)
runLinter()
function removeLinter(name)
linters[name] = nil
end
function runLinter()
local ft = CurView().Buf:FileType()
local file = CurView().Buf.Path
local dir = DirectoryName(file)
if OS == "windows" then
function init()
local devnull = "/dev/null"
if runtime.GOOS == "windows" then
devnull = "NUL"
else
devnull = "/dev/null"
end
if ft == "c" then
lint("gcc", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", file}, "%f:%l:%d+:.+: %m")
elseif ft == "c++" then
lint("gcc", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", file}, "%f:%l:%d+:.+: %m")
elseif ft == "d" then
lint("dmd", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", file}, "%f%(%l%):.+: %m")
elseif ft == "go" then
lint("gobuild", "go", {"build", "-o", devnull}, "%f:%l: %m")
lint("golint", "golint", {file}, "%f:%l:%d+: %m")
elseif ft == "java" then
lint("javac", "javac", {"-d", dir, file}, "%f:%l: error: %m")
elseif ft == "javascript" then
lint("jshint", "jshint", {file}, "%f: line %l,.+, %m")
elseif string.match(ft, "literate") then
lint("literate", "lit", {"-c", file}, "%f:%l:%m")
elseif ft == "lua" then
lint("luacheck", "luacheck", {"--no-color", file}, "%f:%l:%d+: %m")
elseif ft == "nim" then
lint("nim", "nim", {"check", "--listFullPaths", "--stdout", "--hints:off", file}, "%f.%l, %d+. %m")
elseif ft == "Objective-C" then
lint("clang", "xcrun", {"clang", "-fsyntax-only", "-Wall", "-Wextra", file}, "%f:%l:%d+:.+: %m")
elseif ft == "python" then
lint("pyflakes", "pyflakes", {file}, "%f:%l:.-:? %m")
lint("mypy", "mypy", {file}, "%f:%l: %m")
lint("pylint", "pylint", {"--output-format=parseable", "--reports=no", file}, "%f:%l: %m")
elseif ft == "shell" then
lint("shfmt", "shfmt", {file}, "%f:%l:%d+: %m")
elseif ft == "swift" and OS == "darwin" then
lint("switfc", "xcrun", {"swiftc", file}, "%f:%l:%d+:.+: %m")
elseif ft == "swift" and OS == "linux" then
lint("switfc", "swiftc", {file}, "%f:%l:%d+:.+: %m")
elseif ft == "yaml" then
lint("yaml", "yamllint", {"--format", "parsable", file}, "%f:%l:%d+:.+ %m")
makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%d+:.+: %m")
makeLinter("gcc", "c++", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", "%f"}, "%f:%l:%d+:.+: %m")
makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m")
makeLinter("gobuild", "go", "go", {"build", "-o", devnull}, "%f:%l: %m")
makeLinter("golint", "go", "golint", {"%f"}, "%f:%l:%d+: %m")
makeLinter("javac", "java", "javac", {"-d", "%d", "%f"}, "%f:%l: error: %m")
makeLinter("jshint", "javascript", "jshint", {"%f"}, "%f: line %l,.+, %m")
makeLinter("literate", "literate", "lit", {"-c", "%f"}, "%f:%l:%m", {}, false, true)
makeLinter("luacheck", "lua", "luacheck", {"--no-color", "%f"}, "%f:%l:%d+: %m")
makeLinter("nim", "nim", "nim", {"check", "--listFullPaths", "--stdout", "--hints:off", "%f"}, "%f.%l, %d+. %m")
makeLinter("clang", "objective-c", "xcrun", {"clang", "-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%d+:.+: %m")
makeLinter("pyflakes", "python", "pyflakes", {"%f"}, "%f:%l:.-:? %m")
makeLinter("mypy", "python", "mypy", {"%f"}, "%f:%l: %m")
makeLinter("pylint", "python", "pylint", {"--output-format=parseable", "--reports=no", "%f"}, "%f:%l: %m")
makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%d+: %m")
makeLinter("switfc", "swift", "xcrun", {"swiftc", "%f"}, "%f:%l:%d+:.+: %m", {"darwin"}, true)
makeLinter("switfc", "swiftc", {"%f"}, "%f:%l:%d+:.+: %m", {"linux"}, true)
makeLinter("yaml", "yaml", "yamllint", {"--format", "parsable", "%f"}, "%f:%l:%d+:.+ %m")
end
function contains(list, element)
for k, v in pairs(list) do
if v == element then
return true
end
end
return false
end
function runLinter(buf)
local ft = buf:FileType()
local file = buf.Path
local dir = filepath.Dir(file)
for k, v in pairs(linters) do
local ftmatch = ft == v.filetype
if v.domatch then
ftmatch = string.match(ft, v.filetype)
end
local hasOS = contains(v.os, runtime.GOOS)
if not hasOS and v.whitelist then
ftmatch = false
end
if hasOS and not v.whitelist then
ftmatch = false
end
for k, arg in pairs(v.args) do
v.args[k] = arg:gsub("%%f", file):gsub("%%d", dir)
end
if ftmatch then
lint(buf, k, v.cmd, v.args, v.errorformat)
end
end
end
function onSave(view)
if GetOption("linter") then
runLinter()
else
CurView():ClearAllGutterMessages()
end
function onSave(bp)
micro.Log("SAVE")
runLinter(bp.Buf)
return false
end
function lint(linter, cmd, args, errorformat)
CurView():ClearGutterMessages(linter)
function lint(buf, linter, cmd, args, errorformat)
buf:ClearMessages("linter")
JobSpawn(cmd, args, "", "", "linter.onExit", linter, errorformat)
shell.JobSpawn(cmd, args, "", "", "linter.onExit", buf, linter, errorformat)
end
function onExit(output, linter, errorformat)
function onExit(output, buf, linter, errorformat)
micro.Log("ONEXIT")
micro.Log(output)
local lines = split(output, "\n")
local regex = errorformat:gsub("%%f", "(..-)"):gsub("%%l", "(%d+)"):gsub("%%m", "(.+)")
@@ -78,8 +129,9 @@ function onExit(output, linter, errorformat)
line = line:match("^%s*(.+)%s*$")
if string.find(line, regex) then
local file, line, msg = string.match(line, regex)
if basename(CurView().Buf.Path) == basename(file) then
CurView():GutterMessage(linter, tonumber(line), msg, 2)
if basename(buf.Path) == basename(file) then
local bmsg = buffer.NewMessageAtLine("linter", msg, tonumber(line), buffer.MTError)
buf:AddMessage(bmsg)
end
end
end
@@ -96,7 +148,7 @@ end
function basename(file)
local sep = "/"
if OS == "windows" then
if runtime.GOOS == "windows" then
sep = "\\"
end
local name = string.gsub(file, "(.*" .. sep .. ")(.*)", "%2")