mirror of
https://github.com/zyedidia/micro.git
synced 2026-02-04 22:20:20 +09:00
Add linter plugin support
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
if option == pl.Name {
|
||||
if nativeValue.(bool) && !pl.Loaded {
|
||||
pl.Load()
|
||||
pl.Call("init")
|
||||
} else if !nativeValue.(bool) && pl.Loaded {
|
||||
pl.Call("deinit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user