]> git.lizzy.rs Git - micro.git/commitdiff
Start plugin support and plugin manager
authorZachary Yedidia <zyedidia@gmail.com>
Sat, 26 Jan 2019 03:33:45 +0000 (22:33 -0500)
committerZachary Yedidia <zyedidia@gmail.com>
Wed, 25 Dec 2019 22:05:10 +0000 (17:05 -0500)
cmd/micro/lua/lua.go
cmd/micro/lua/plugin.go [new file with mode: 0644]
cmd/micro/manager/fetch.go [new file with mode: 0644]
cmd/micro/manager/manager_test.go [new file with mode: 0644]
cmd/micro/manager/parser.go [new file with mode: 0644]

index 7fa44a8083336d7f571bf0034dc876aa2cbf9889..853f10fa05c041411bb4425a71cfa2173b304403 100644 (file)
@@ -1,6 +1,7 @@
 package lua
 
 import (
+       "bytes"
        "errors"
        "fmt"
        "io"
@@ -28,10 +29,10 @@ func init() {
 }
 
 // LoadFile loads a lua file
-func LoadFile(module string, file string, data string) error {
-       pluginDef := "local P = {};" + module + " = P;setmetatable(" + module + ", {__index = _G});setfenv(1, P);"
+func LoadFile(module string, file string, data []byte) error {
+       pluginDef := []byte("module(\"" + module + "\", package.seeall)")
 
-       if fn, err := L.Load(strings.NewReader(pluginDef+data), file); err != nil {
+       if fn, err := L.Load(bytes.NewReader(append(pluginDef, data...)), file); err != nil {
                return err
        } else {
                L.Push(fn)
diff --git a/cmd/micro/lua/plugin.go b/cmd/micro/lua/plugin.go
new file mode 100644 (file)
index 0000000..d68003c
--- /dev/null
@@ -0,0 +1,67 @@
+package lua
+
+import (
+       "errors"
+       "io/ioutil"
+       "strings"
+
+       lua "github.com/yuin/gopher-lua"
+)
+
+var ErrNoSuchFunction = errors.New("No such function exists")
+
+type Plugin struct {
+       name  string
+       files []string
+}
+
+func NewPluginFromDir(name string, dir string) (*Plugin, error) {
+       files, err := ioutil.ReadDir(dir)
+       if err != nil {
+               return nil, err
+       }
+
+       p := new(Plugin)
+       p.name = name
+
+       for _, f := range files {
+               if strings.HasSuffix(f.Name(), ".lua") {
+                       p.files = append(p.files, dir+f.Name())
+               }
+       }
+
+       return p, nil
+}
+
+func (p *Plugin) Load() error {
+       for _, f := range p.files {
+               dat, err := ioutil.ReadFile(f)
+               if err != nil {
+                       return err
+               }
+               err = LoadFile(p.name, f, dat)
+               if err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
+       plug := L.GetGlobal(p.name)
+       luafn := L.GetField(plug, fn)
+       if luafn == lua.LNil {
+               return nil, ErrNoSuchFunction
+       }
+       err := L.CallByParam(lua.P{
+               Fn:      luafn,
+               NRet:    1,
+               Protect: true,
+       }, args...)
+       if err != nil {
+               return nil, err
+       }
+       ret := L.Get(-1)
+       L.Pop(1)
+       return ret, nil
+}
diff --git a/cmd/micro/manager/fetch.go b/cmd/micro/manager/fetch.go
new file mode 100644 (file)
index 0000000..9724567
--- /dev/null
@@ -0,0 +1,46 @@
+package manager
+
+import (
+       "io/ioutil"
+       "net/http"
+       "path"
+
+       "github.com/zyedidia/micro/cmd/micro/config"
+       git "gopkg.in/src-d/go-git.v4"
+)
+
+// NewPluginInfoFromUrl creates a new PluginInfo from a URL by fetching
+// the data at that URL and parsing the JSON (running a GET request at
+// the URL should return the JSON for a plugin info)
+func NewPluginInfoFromUrl(url string) (*PluginInfo, error) {
+       resp, err := http.Get(url)
+       if err != nil {
+               return nil, err
+       }
+
+       defer resp.Body.Close()
+       dat, err := ioutil.ReadAll(resp.Body)
+       if err != nil {
+               return nil, err
+       }
+
+       return NewPluginInfo(dat)
+}
+
+// FetchRepo downloads this plugin's git repository
+func (i *PluginInfo) FetchRepo() error {
+       _, err := git.PlainClone(path.Join(config.ConfigDir, "plugin", i.Name), false, &git.CloneOptions{
+               URL:      i.Repo,
+               Progress: nil,
+       })
+
+       return err
+}
+
+func (i *PluginInfo) FetchDeps() error {
+       return nil
+}
+
+func (i *PluginInfo) PostInstallHooks() error {
+       return nil
+}
diff --git a/cmd/micro/manager/manager_test.go b/cmd/micro/manager/manager_test.go
new file mode 100644 (file)
index 0000000..d39e1f1
--- /dev/null
@@ -0,0 +1,72 @@
+package manager
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/zyedidia/micro/cmd/micro/config"
+)
+
+func init() {
+       config.InitConfigDir("./")
+}
+
+var sampleJson = []byte(`{
+    "name": "comment",
+    "description": "Plugin to auto comment or uncomment lines",
+    "website": "https://github.com/micro-editor/comment-plugin",
+       "repository": "https://github.com/micro-editor/comment-plugin",
+    "versions": [
+        {
+            "version": "1.0.6",
+            "tag": "v1.0.6",
+            "require": {
+                "micro": ">=1.1.0"
+            }
+        },
+        {
+            "version": "1.0.5",
+            "tag": "v1.0.5",
+            "require": {
+                "micro": ">=1.0.0"
+            }
+        },
+        {
+            "version": "1.0.6-dev",
+            "tag": "nightly",
+            "require": {
+                "micro": ">=1.3.1"
+            }
+        }
+    ]
+}`)
+
+func TestParse(t *testing.T) {
+       _, err := NewPluginInfo(sampleJson)
+       if err != nil {
+               t.Error(err)
+       }
+}
+
+// func TestFetch(t *testing.T) {
+//     i, err := NewPluginInfoFromUrl("http://zbyedidia.webfactional.com/micro/test.json")
+//     if err != nil {
+//             t.Error(err)
+//     }
+//
+//     err = i.FetchRepo()
+//     if err != nil {
+//             t.Error(err)
+//     }
+// }
+
+func TestList(t *testing.T) {
+       is, err := ListInstalledPlugins()
+       if err != nil {
+               t.Error(err)
+       }
+
+       for _, i := range is {
+               fmt.Println(i.Name)
+       }
+}
diff --git a/cmd/micro/manager/parser.go b/cmd/micro/manager/parser.go
new file mode 100644 (file)
index 0000000..d4eb63b
--- /dev/null
@@ -0,0 +1,149 @@
+package manager
+
+import (
+       "bytes"
+       "encoding/json"
+       "errors"
+       "io/ioutil"
+       "path"
+
+       "github.com/blang/semver"
+       "github.com/zyedidia/micro/cmd/micro/config"
+       git "gopkg.in/src-d/go-git.v4"
+)
+
+var (
+       ErrMissingName     = errors.New("Missing or empty name field")
+       ErrMissingDesc     = errors.New("Missing or empty description field")
+       ErrMissingSite     = errors.New("Missing or empty website field")
+       ErrMissingRepo     = errors.New("Missing or empty repository field")
+       ErrMissingVersions = errors.New("Missing or empty versions field")
+       ErrMissingTag      = errors.New("Missing or empty tag field")
+       ErrMissingRequire  = errors.New("Missing or empty require field")
+)
+
+type Plugin struct {
+       info *PluginInfo
+       dir  string
+       repo *git.Repository
+       // Index into info.Versions showing the current version of this plugin
+       Version int
+}
+
+// PluginVersion describes a version for a plugin as well as any dependencies that
+// version might have
+// This marks a tag that corresponds to the version in the git repo
+type PluginVersion struct {
+       Vers    semver.Version
+       Vstr    string            `json:"version"`
+       Tag     string            `json:"tag"`
+       Require map[string]string `json:"require"`
+}
+
+// PluginInfo contains all the needed info about a plugin
+type PluginInfo struct {
+       Name     string          `json:"name"`
+       Desc     string          `json:"description"`
+       Site     string          `json:"website"`
+       Repo     string          `json:"repository"`
+       Versions []PluginVersion `json:"versions"`
+}
+
+// NewPluginInfo parses a JSON input into a valid PluginInfo struct
+// Returns an error if there are any missing fields or any invalid fields
+// There are no optional fields in a plugin info json file
+func NewPluginInfo(data []byte) (*PluginInfo, error) {
+       var info PluginInfo
+
+       dec := json.NewDecoder(bytes.NewReader(data))
+       dec.DisallowUnknownFields() // Force errors
+
+       if err := dec.Decode(&info); err != nil {
+               return nil, err
+       }
+
+       if len(info.Name) == 0 {
+               return nil, ErrMissingName
+       } else if len(info.Desc) == 0 {
+               return nil, ErrMissingDesc
+       } else if len(info.Site) == 0 {
+               return nil, ErrMissingSite
+       } else if len(info.Repo) == 0 {
+               return nil, ErrMissingRepo
+       } else if err := info.makeVersions(); err != nil {
+               return nil, err
+       }
+
+       return &info, nil
+}
+
+func (i *PluginInfo) makeVersions() error {
+       if len(i.Versions) == 0 {
+               return ErrMissingVersions
+       }
+
+       for _, v := range i.Versions {
+               sv, err := semver.Make(v.Vstr)
+               if err != nil {
+                       return err
+               }
+               v.Vers = sv
+               if len(v.Tag) == 0 {
+                       return ErrMissingTag
+               } else if v.Require == nil {
+                       return ErrMissingRequire
+               }
+       }
+
+       return nil
+}
+
+// InstalledPlugins searches the config directory for all installed plugins
+// and returns the list of plugin infos corresponding to them
+func ListInstalledPlugins() ([]*Plugin, error) {
+       pdir := path.Join(config.ConfigDir, "plugin")
+
+       files, err := ioutil.ReadDir(pdir)
+       if err != nil {
+               return nil, err
+       }
+
+       var plugins []*Plugin
+
+       for _, dir := range files {
+               if dir.IsDir() {
+                       files, err := ioutil.ReadDir(path.Join(pdir, dir.Name()))
+                       if err != nil {
+                               return nil, err
+                       }
+
+                       for _, f := range files {
+                               if f.Name() == "repo.json" {
+                                       dat, err := ioutil.ReadFile(path.Join(pdir, dir.Name(), "repo.json"))
+                                       if err != nil {
+                                               return nil, err
+                                       }
+                                       info, err := NewPluginInfo(dat)
+                                       if err != nil {
+                                               return nil, err
+                                       }
+
+                                       dirname := path.Join(pdir, dir.Name())
+                                       r, err := git.PlainOpen(dirname)
+                                       if err != nil {
+                                               return nil, err
+                                       }
+
+                                       p := &Plugin{
+                                               info: info,
+                                               dir:  dirname,
+                                               repo: r,
+                                       }
+
+                                       plugins = append(plugins, p)
+                               }
+                       }
+               }
+       }
+       return plugins, nil
+}