├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── _example ├── dev │ ├── child │ │ └── child.go │ └── job │ │ ├── job.go │ │ └── job.vim └── echo │ ├── echo.go │ ├── echo.vim │ └── echo_test.go ├── example_test.go ├── io.go ├── logger.go ├── scripts └── install-vim.sh ├── server.go ├── vimclient.go ├── vimclient_test.go └── vimserver.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | - tip 6 | 7 | os: 8 | - linux 9 | # - osx 10 | 11 | install: 12 | - go get -d -v -t ./... 13 | - go get github.com/mattn/goveralls 14 | - bash scripts/install-vim.sh 15 | - export PATH=$HOME/vim/bin:$PATH 16 | 17 | before_script: 18 | - uname -a 19 | - which -a vim 20 | - vim --cmd version --cmd quit 21 | - go env 22 | 23 | script: 24 | - go test -v -race ./... 25 | - goveralls -service=travis-ci 26 | - make lint 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 haya14busa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Prefer go commands for basic tasks like `build`, `test`, etc... 2 | 3 | .PHONY: lintdeps lint 4 | 5 | lintdeps: 6 | go get github.com/golang/lint/golint 7 | go get github.com/kisielk/errcheck 8 | go get github.com/client9/misspell/cmd/misspell 9 | go get honnef.co/go/unused/cmd/unused 10 | 11 | lint: lintdeps 12 | ! gofmt -d -s . | grep '^' # exit 1 if any output given 13 | golint -set_exit_status ./... 14 | go vet ./... 15 | errcheck -asserts -ignoretests -ignore 'Close|Start|Serve|Remove' 16 | misspell -error **/*.go **/*.md 17 | unused ./... 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## vim-go-client - Vim 8.0 client written in go 2 | 3 | [![Travis Build Status](https://travis-ci.org/haya14busa/vim-go-client.svg?branch=master)](https://travis-ci.org/haya14busa/vim-go-client) 4 | [![Coverage Status](https://coveralls.io/repos/github/haya14busa/vim-go-client/badge.svg?branch=master)](https://coveralls.io/github/haya14busa/vim-go-client?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/haya14busa/vim-go-client)](https://goreportcard.com/report/github.com/haya14busa/vim-go-client) 6 | [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 7 | [![GoDoc](https://godoc.org/github.com/haya14busa/vim-go-client?status.svg)](https://godoc.org/github.com/haya14busa/vim-go-client) 8 | 9 | WIP 10 | 11 | ### :bird: Author 12 | haya14busa (https://github.com/haya14busa) 13 | -------------------------------------------------------------------------------- /_example/dev/child/child.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | vim "github.com/haya14busa/vim-go-client" 12 | ) 13 | 14 | type myHandler struct{} 15 | 16 | func (h *myHandler) Serve(cli *vim.Client, msg *vim.Message) { 17 | log.Printf("receive: %#v", msg) 18 | if msg.MsgID > 0 { 19 | start := time.Now() 20 | log.Println(cli.Expr("eval(join(range(10), '+'))")) 21 | log.Printf("cli.Expr: finished in %v", time.Now().Sub(start)) 22 | } 23 | } 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | cli, closer, err := vim.NewChildClient(&myHandler{}, nil) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer closer.Close() 33 | 34 | fmt.Println(cli.Ex("echom 'hi'")) 35 | 36 | cli.Redraw("") 37 | cli.Redraw("force") 38 | 39 | cli.Normal("gg") 40 | 41 | { 42 | start := time.Now() 43 | for i := 0; i < 3; i++ { 44 | fmt.Println(cli.Expr(fmt.Sprintf("1+%d", i))) 45 | } 46 | log.Printf("cli.Expr * 3: finished in %v", time.Now().Sub(start)) 47 | } 48 | 49 | { 50 | start := time.Now() 51 | fmt.Println(cli.Call("matchstr", "testing", "ing")) 52 | fmt.Println(cli.Call("matchstr", "testing", "ing", 2)) 53 | fmt.Println(cli.Call("matchstr", "testing", "ing", 5)) 54 | log.Printf("cli.Call: finished in %v", time.Now().Sub(start)) 55 | } 56 | 57 | if err := cli.Ex("hoge"); err != nil { 58 | fmt.Printf(":hoge error: %v\n", err) 59 | } 60 | 61 | if err := cli.Ex("echom 'hi'"); err != nil { 62 | fmt.Printf(":echom error: %v\n", err) 63 | } 64 | 65 | scanner := bufio.NewScanner(os.Stdin) 66 | for scanner.Scan() { 67 | log.Printf("send: %v", scanner.Text()) 68 | if _, err := cli.Write(scanner.Bytes()); err != nil { 69 | log.Fatal(err) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /_example/dev/job/job.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | vim "github.com/haya14busa/vim-go-client" 10 | ) 11 | 12 | type myHandler struct{} 13 | 14 | func (h *myHandler) Serve(cli *vim.Client, msg *vim.Message) { 15 | log.Printf("receive: %#v", msg) 16 | if msg.MsgID > 0 { 17 | 18 | if msg.Body == "hi" { 19 | cli.Send(&vim.Message{ 20 | MsgID: msg.MsgID, 21 | Body: "hi how are you?", 22 | }) 23 | } else { 24 | start := time.Now() 25 | log.Println(cli.Expr("eval(join(range(10), '+'))")) 26 | log.Printf("cli.Expr: finished in %v", time.Now().Sub(start)) 27 | } 28 | 29 | } 30 | } 31 | 32 | func main() { 33 | handler := &myHandler{} 34 | cli := vim.NewClient(vim.NewReadWriter(os.Stdin, os.Stdout), handler) 35 | done := make(chan error, 1) 36 | go func() { 37 | done <- cli.Start() 38 | }() 39 | 40 | cli.Ex("echom 'hi'") 41 | log.Println(cli.Expr("1+1")) 42 | 43 | select { 44 | case err := <-done: 45 | fmt.Printf("exit with error: %v\n", err) 46 | fmt.Println("bye;)") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /_example/dev/job/job.vim: -------------------------------------------------------------------------------- 1 | call ch_logfile('/tmp/vimchannellog', 'w') 2 | let s:target = expand(':r') . '.go' 3 | let s:cmd = 'go run ' . s:target 4 | " let s:target = expand(':r') 5 | " let s:cmd = s:target 6 | 7 | function! s:err_cb(...) abort 8 | echom '---err_cb---' 9 | echom string(a:000) 10 | endfunction 11 | 12 | let s:option = { 13 | \ 'in_mode': 'json', 14 | \ 'out_mode': 'json', 15 | \ 16 | \ 'err_cb': function('s:err_cb'), 17 | \ } 18 | 19 | if !exists('g:job') 20 | let g:job = job_start(s:cmd, s:option) 21 | endif 22 | 23 | if !exists('g:ch') 24 | let g:ch = job_getchannel(g:job) 25 | endif 26 | 27 | echo job_info(g:job) 28 | echo ch_info(g:ch) 29 | 30 | function! s:cb(...) abort 31 | echom '---cb---' 32 | echom string(a:000) 33 | endfunction 34 | 35 | call ch_evalexpr(g:ch, 'hi') 36 | call ch_evalexpr(g:job, 'hi') 37 | call ch_sendexpr(g:ch, 'hi', {'callback': function('s:cb')}) 38 | call ch_sendexpr(g:job, 'hi', {'callback': function('s:cb')}) 39 | -------------------------------------------------------------------------------- /_example/echo/echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | vim "github.com/haya14busa/vim-go-client" 7 | ) 8 | 9 | type echoHandler struct{} 10 | 11 | func (h *echoHandler) Serve(cli *vim.Client, msg *vim.Message) { 12 | cli.Send(msg) 13 | } 14 | 15 | func main() { 16 | handler := &echoHandler{} 17 | cli := vim.NewClient(vim.NewReadWriter(os.Stdin, os.Stdout), handler) 18 | cli.Start() 19 | } 20 | -------------------------------------------------------------------------------- /_example/echo/echo.vim: -------------------------------------------------------------------------------- 1 | call ch_logfile('/tmp/vimchannellog', 'w') 2 | let s:target = expand(':r') . '.go' 3 | let s:cmd = 'go run ' . s:target 4 | let s:option = { 5 | \ 'in_mode': 'json', 6 | \ 'out_mode': 'json', 7 | \ } 8 | let s:job = job_start(s:cmd, s:option) 9 | echom ch_evalexpr(s:job, 'hi!') 10 | " => hi! 11 | 12 | let s:done = 0 13 | 14 | function! s:cb(ch, msg) abort 15 | let s:done = 1 16 | echom string(a:msg) 17 | endfunction 18 | 19 | call ch_sendexpr(s:job, {'msg': 'hi!'}, {'callback': function('s:cb')}) 20 | 21 | while 1 22 | if s:done 23 | call job_stop(s:job) 24 | break 25 | endif 26 | sleep 10ms 27 | endwhile 28 | -------------------------------------------------------------------------------- /_example/echo/echo_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | vim "github.com/haya14busa/vim-go-client" 7 | ) 8 | 9 | type myHandler struct{} 10 | 11 | func (h *myHandler) Serve(cli *vim.Client, msg *vim.Message) {} 12 | 13 | const wantMes = ` 14 | Messages maintainer: Bram Moolenaar 15 | hi! 16 | {'msg': 'hi!'}` 17 | 18 | func TestExampleEcho(t *testing.T) { 19 | cli, closer, err := vim.NewChildClient(&myHandler{}, nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer closer.Close() 24 | 25 | err = cli.Ex(":message clear") 26 | err = cli.Ex(":source $GOPATH/src/github.com/haya14busa/vim-go-client/example/echo/echo.vim") 27 | mes, err := cli.Expr("execute(':message')") 28 | if err != nil { 29 | t.Error(err) 30 | } 31 | got, ok := mes.(string) 32 | if !ok { 33 | t.Fatal("mes should be string") 34 | } 35 | if got != wantMes { 36 | t.Errorf("got:\n%v\nwant:\n%v", got, wantMes) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package vim_test 2 | 3 | import ( 4 | "os" 5 | 6 | vim "github.com/haya14busa/vim-go-client" 7 | ) 8 | 9 | type echoHandler struct{} 10 | 11 | func (h *echoHandler) Serve(cli *vim.Client, msg *vim.Message) { 12 | cli.Send(msg) 13 | } 14 | 15 | func ExampleNewClient_job() { 16 | // see example/echo/ for working example. 17 | handler := &echoHandler{} 18 | cli := vim.NewClient(vim.NewReadWriter(os.Stdin, os.Stdout), handler) 19 | cli.Start() 20 | } 21 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package vim 2 | 3 | import "io" 4 | 5 | // NewReadWriter returns simple io.ReadWriter. 6 | // bufio.ReadWriter has buffers and needs to be flushed., so we cannot use 7 | // bufio.NewReadWriter() for Vim client which accept io.ReadWriter. 8 | // ref: https://groups.google.com/forum/#!topic/golang-nuts/OJnnwlfsPCc 9 | func NewReadWriter(r io.Reader, w io.Writer) io.ReadWriter { 10 | return &readWriter{r, w} 11 | } 12 | 13 | type readWriter struct { 14 | io.Reader 15 | io.Writer 16 | } 17 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package vim 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var logger = log.New(os.Stderr, "", log.LstdFlags) 9 | 10 | // SetLogger sets the logger that is used in go process. Call only from 11 | // init() functions. 12 | func SetLogger(l *log.Logger) { 13 | logger = l 14 | } 15 | -------------------------------------------------------------------------------- /scripts/install-vim.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://github.com/vim-jp/vital.vim/blob/bff0d8c58c1fb6ab9e4a9fc0c672368502f10d88/scripts/install-vim.sh 3 | set -e 4 | 5 | git clone --depth 1 https://github.com/vim/vim /tmp/vim 6 | cd /tmp/vim 7 | ./configure --prefix="$HOME/vim" --with-features=huge --enable-fail-if-missing 8 | make -j2 9 | make install 10 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package vim 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | ) 7 | 8 | // ErrTimeOut represents time out error. 9 | var ErrTimeOut = errors.New("time out") 10 | 11 | // Body represents Message body. e.g. {expr} of `:h ch_sendexpr()` 12 | type Body interface{} 13 | 14 | // Message represents rpc message type of JSON channel. `:h channel-use`. 15 | type Message struct { 16 | MsgID int 17 | Body Body 18 | } 19 | 20 | // Handler represents go server handler to handle message from Vim. 21 | type Handler interface { 22 | Serve(*Client, *Message) 23 | } 24 | 25 | // Server represents go server. 26 | type Server struct { 27 | Handler Handler // handler to invoke 28 | } 29 | 30 | // Serve starts go server. 31 | func (srv *Server) Serve(l net.Listener) error { 32 | defer l.Close() 33 | for { 34 | // Wait for a connection. 35 | conn, err := l.Accept() 36 | if err != nil { 37 | if ne, ok := err.(net.Error); ok && ne.Temporary() { 38 | logger.Println(err) 39 | continue 40 | } 41 | return err 42 | } 43 | 44 | cli := NewClient(conn, srv.Handler) 45 | 46 | // Handle the connection in a new goroutine. 47 | // The loop then returns to accepting, so that 48 | // multiple connections may be served concurrently. 49 | go cli.Start() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /vimclient.go: -------------------------------------------------------------------------------- 1 | // Package vim provides Vim client and server implementations. 2 | // You can start Vim as a server as a child process or connect to Vim, and 3 | // communicate with it via TCP or stdin/stdout. 4 | // :h channel.txt 5 | package vim 6 | 7 | import ( 8 | "bufio" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "math/rand" 14 | "net" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // ErrExpr represents "expr" command error. 20 | var ErrExpr = errors.New("the evaluation fails or the result can't be encoded in JSON") 21 | 22 | // Client represents Vim client. 23 | type Client struct { 24 | // rw is readwriter for communication between go-server and vim-server. 25 | rw io.ReadWriter 26 | 27 | // handler handles message from Vim. 28 | handler Handler 29 | 30 | // responses handles response from Vim. 31 | responses map[int]chan Body // TODO: need lock? 32 | } 33 | 34 | // ChildCliCloser is closer of child Vim client process. 35 | type ChildCliCloser struct { 36 | listener net.Listener 37 | process *Process 38 | } 39 | 40 | // Close closes child Vim client process. 41 | func (c *ChildCliCloser) Close() error { 42 | var err error 43 | err = c.process.Close() 44 | err = c.listener.Close() 45 | return err 46 | } 47 | 48 | var _ Handler = &getCliHandler{} 49 | 50 | // getCliHandler is handler to get one connected Vim client. 51 | type getCliHandler struct { 52 | handler Handler 53 | 54 | connected bool 55 | connectedMu sync.RWMutex 56 | chCli chan *Client 57 | } 58 | 59 | func (h *getCliHandler) Serve(cli *Client, msg *Message) { 60 | h.connectedMu.Lock() 61 | if !h.connected { 62 | h.chCli <- cli 63 | h.connected = true 64 | } 65 | h.connectedMu.Unlock() 66 | h.handler.Serve(cli, msg) 67 | } 68 | 69 | // NewClient creates Vim client. 70 | func NewClient(rw io.ReadWriter, handler Handler) *Client { 71 | return &Client{ 72 | rw: rw, 73 | handler: handler, 74 | 75 | responses: make(map[int]chan Body), 76 | } 77 | } 78 | 79 | // NewChildClient creates connected child process Vim client. 80 | func NewChildClient(handler Handler, args []string) (*Client, *ChildCliCloser, error) { 81 | l, err := net.Listen("tcp", "localhost:0") 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | 86 | p, err := NewChildVimServer(l.Addr().String(), args) 87 | if err != nil { 88 | return nil, nil, err 89 | } 90 | 91 | h := &getCliHandler{ 92 | handler: handler, 93 | chCli: make(chan *Client), 94 | } 95 | 96 | server := &Server{Handler: h} 97 | go server.Serve(l) 98 | 99 | closer := &ChildCliCloser{ 100 | listener: l, 101 | process: p, 102 | } 103 | 104 | select { 105 | case cli := <-h.chCli: 106 | return cli, closer, nil 107 | case <-time.After(15 * time.Second): 108 | closer.Close() 109 | return nil, nil, ErrTimeOut 110 | } 111 | } 112 | 113 | // Send sends message to Vim. 114 | func (cli *Client) Send(msg *Message) error { 115 | v := [2]interface{}{msg.MsgID, msg.Body} 116 | return json.NewEncoder(cli.rw).Encode(v) 117 | } 118 | 119 | // Write writes raw message to Vim. 120 | func (cli *Client) Write(p []byte) (n int, err error) { 121 | return cli.rw.Write(p) 122 | } 123 | 124 | // Redraw runs command "redraw" (:h channel-commands). 125 | func (cli *Client) Redraw(force string) error { 126 | v := []interface{}{"redraw", force} 127 | return json.NewEncoder(cli.rw).Encode(v) 128 | } 129 | 130 | // Ex runs command "ex" (:h channel-commands). 131 | func (cli *Client) Ex(cmd string) error { 132 | var err error 133 | encoder := json.NewEncoder(cli.rw) 134 | err = encoder.Encode([]interface{}{"ex", cmd}) 135 | return err 136 | } 137 | 138 | // Normal runs command "normal" (:h channel-commands). 139 | func (cli *Client) Normal(ncmd string) error { 140 | v := []interface{}{"normal", ncmd} 141 | return json.NewEncoder(cli.rw).Encode(v) 142 | } 143 | 144 | // Expr runs command "expr" (:h channel-commands). 145 | func (cli *Client) Expr(expr string) (Body, error) { 146 | n := cli.prepareResp() 147 | v := []interface{}{"expr", expr, n} 148 | if err := json.NewEncoder(cli.rw).Encode(v); err != nil { 149 | return nil, err 150 | } 151 | body, err := cli.waitResp(n) 152 | if err != nil { 153 | return nil, err 154 | } 155 | if fmt.Sprintf("%s", body) == "ERROR" { 156 | return nil, ErrExpr 157 | } 158 | return body, nil 159 | } 160 | 161 | // Call runs command "call" (:h channel-commands). 162 | func (cli *Client) Call(funcname string, args ...interface{}) (Body, error) { 163 | n := cli.prepareResp() 164 | v := []interface{}{"call", funcname, args, n} 165 | if err := json.NewEncoder(cli.rw).Encode(v); err != nil { 166 | return nil, err 167 | } 168 | return cli.waitResp(n) 169 | } 170 | 171 | // prepareResp prepares response from Vim and returns negative number. 172 | // Server.waitResp can wait and get the response. 173 | func (cli *Client) prepareResp() int { 174 | if cli.responses == nil { 175 | cli.responses = make(map[int]chan Body) 176 | } 177 | for { 178 | n := -int(rand.Int31()) 179 | if _, ok := cli.responses[n]; ok { 180 | continue 181 | } 182 | cli.responses[n] = make(chan Body, 1) 183 | return n 184 | } 185 | } 186 | 187 | // fillResp fills response which is prepared by Server.prepareResp(). 188 | func (cli *Client) fillResp(n int, body Body) { 189 | if ch, ok := cli.responses[n]; ok { 190 | ch <- body 191 | } 192 | } 193 | 194 | // waitResp waits response which is prepared by Server.prepareResp(). 195 | func (cli *Client) waitResp(n int) (Body, error) { 196 | select { 197 | case body := <-cli.responses[n]: 198 | delete(cli.responses, n) 199 | return body, nil 200 | case <-time.After(1 * time.Minute): 201 | return nil, ErrTimeOut 202 | } 203 | } 204 | 205 | // Start starts to wait message from Vim. 206 | func (cli *Client) Start() error { 207 | scanner := bufio.NewScanner(cli.rw) 208 | for scanner.Scan() { 209 | msg, err := unmarshalMsg(scanner.Bytes()) 210 | if err != nil { 211 | // TODO: handler err 212 | logger.Println(err) 213 | continue 214 | } 215 | if msg.MsgID < 0 { 216 | cli.fillResp(msg.MsgID, msg.Body) 217 | } 218 | go cli.handler.Serve(cli, msg) 219 | } 220 | if err := scanner.Err(); err != nil { 221 | logger.Println(err) 222 | return err 223 | } 224 | return nil 225 | } 226 | 227 | // unmarshalMsg unmarshals json message from Vim. 228 | // msg format: [{number},{expr}] 229 | func unmarshalMsg(data []byte) (msg *Message, err error) { 230 | var vimMsg [2]interface{} 231 | if err := json.Unmarshal(data, &vimMsg); err != nil { 232 | return nil, fmt.Errorf("fail to decode vim message: %v", err) 233 | } 234 | number, ok := vimMsg[0].(float64) 235 | if !ok { 236 | return nil, fmt.Errorf("expects message ID, but got %+v", vimMsg[0]) 237 | } 238 | m := &Message{ 239 | MsgID: int(number), 240 | Body: vimMsg[1], 241 | } 242 | return m, nil 243 | } 244 | -------------------------------------------------------------------------------- /vimclient_test.go: -------------------------------------------------------------------------------- 1 | package vim_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | vim "github.com/haya14busa/vim-go-client" 17 | ) 18 | 19 | var cli *vim.Client 20 | 21 | var defaultServeFunc = func(cli *vim.Client, msg *vim.Message) {} 22 | 23 | var vimArgs = []string{"-Nu", "NONE", "-i", "NONE", "-n"} 24 | 25 | var waitLog = func() { time.Sleep(1 * time.Millisecond) } 26 | 27 | type testHandler struct { 28 | f func(cli *vim.Client, msg *vim.Message) 29 | } 30 | 31 | func (h *testHandler) Serve(cli *vim.Client, msg *vim.Message) { 32 | fn := h.f 33 | if fn == nil { 34 | fn = defaultServeFunc 35 | } 36 | fn(cli, msg) 37 | } 38 | 39 | func TestMain(m *testing.M) { 40 | c, closer, err := vim.NewChildClient(&testHandler{}, vimArgs) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | cli = c 45 | code := m.Run() 46 | closer.Close() 47 | os.Exit(code) 48 | } 49 | 50 | func BenchmarkNewChildClient(b *testing.B) { 51 | for i := 0; i < b.N; i++ { 52 | _, closer, err := vim.NewChildClient(&testHandler{}, vimArgs) 53 | if err != nil { 54 | b.Fatal(err) 55 | } 56 | defer closer.Close() 57 | } 58 | } 59 | 60 | func TestNewChildClient(t *testing.T) { 61 | serveFuncCalled := false 62 | var serveFuncCalledMu sync.RWMutex 63 | 64 | serveFunc := func(cli *vim.Client, msg *vim.Message) { 65 | // t.Log(msg) 66 | serveFuncCalledMu.Lock() 67 | serveFuncCalled = true 68 | serveFuncCalledMu.Unlock() 69 | } 70 | 71 | cli, closer, err := vim.NewChildClient(&testHandler{f: serveFunc}, vimArgs) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | defer closer.Close() 76 | if _, err = cli.Expr("1 + 1"); err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | serveFuncCalledMu.Lock() 81 | if !serveFuncCalled { 82 | t.Error("serveFunc must be called") 83 | } 84 | serveFuncCalledMu.Unlock() 85 | 86 | status, err := cli.Expr("ch_status(g:vim_go_client_handler)") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if fmt.Sprintf("%s", status) != "open" { 91 | t.Error("channel must be open") 92 | } 93 | } 94 | 95 | func TestClient_Send(t *testing.T) { 96 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | defer tmp.Close() 101 | defer os.Remove(tmp.Name()) 102 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 103 | cli.Send(&vim.Message{MsgID: -1, Body: "hi, how are you?"}) 104 | waitLog() 105 | if !containsString(tmp, "hi, how are you?") { 106 | t.Error("cli.Send should send message to Vim") 107 | } 108 | } 109 | 110 | func TestClient_Write(t *testing.T) { 111 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | defer tmp.Close() 116 | defer os.Remove(tmp.Name()) 117 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 118 | cli.Write([]byte("hi, how are you?")) 119 | waitLog() 120 | if !containsString(tmp, "hi, how are you?") { 121 | t.Error("cli.Write should send message to Vim") 122 | } 123 | } 124 | 125 | func TestClient_Redraw_force(t *testing.T) { 126 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | defer tmp.Close() 131 | defer os.Remove(tmp.Name()) 132 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 133 | cli.Redraw("force") 134 | waitLog() 135 | if !containsString(tmp, ": redraw") { 136 | t.Error(`cli.Redraw("force") should redraw Vim`) 137 | } 138 | } 139 | 140 | func TestClient_Redraw(t *testing.T) { 141 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | defer tmp.Close() 146 | defer os.Remove(tmp.Name()) 147 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 148 | cli.Redraw("") 149 | waitLog() 150 | if !containsString(tmp, ": redraw") { 151 | t.Error(`cli.Redraw("") should redraw`) 152 | } 153 | } 154 | 155 | func TestClient_Ex(t *testing.T) { 156 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | defer tmp.Close() 161 | defer os.Remove(tmp.Name()) 162 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 163 | cli.Ex("echo 'hi'") 164 | waitLog() 165 | if !containsString(tmp, ": Executing ex command") { 166 | t.Error(`cli.Ex() should execute ex command`) 167 | } 168 | } 169 | 170 | func TestClient_Normal(t *testing.T) { 171 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | defer tmp.Close() 176 | defer os.Remove(tmp.Name()) 177 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 178 | cli.Normal("gg") 179 | waitLog() 180 | if !containsString(tmp, ": Executing normal command") { 181 | t.Error(`cli.Normal() should execute normal command`) 182 | } 183 | } 184 | 185 | func TestClient_Expr_log(t *testing.T) { 186 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | defer tmp.Close() 191 | defer os.Remove(tmp.Name()) 192 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 193 | cli.Expr("0") 194 | waitLog() 195 | if !containsString(tmp, ": Evaluating expression") { 196 | t.Error(`cli.Expr() should evaluate expr`) 197 | } 198 | } 199 | 200 | func TestClient_Expr(t *testing.T) { 201 | tests := []struct { 202 | in string 203 | want interface{} 204 | }{ 205 | {in: "1 + 1", want: float64(2)}, 206 | {in: `'hello ' . 'world!'`, want: "hello world!"}, 207 | {in: "{}", want: map[string]interface{}{}}, 208 | {in: "[1,2,3]", want: []interface{}{float64(1), float64(2), float64(3)}}, 209 | {in: "v:false", want: false}, 210 | {in: "v:true", want: true}, 211 | {in: "v:none", want: nil}, 212 | {in: "v:null", want: nil}, 213 | {in: "v:count", want: float64(0)}, 214 | {in: "{x -> x * x}(2)", want: float64(4)}, 215 | } 216 | 217 | for _, tt := range tests { 218 | got, err := cli.Expr(tt.in) 219 | if err != nil { 220 | t.Error(err) 221 | } 222 | if !reflect.DeepEqual(got, tt.want) { 223 | t.Errorf("cli.Expr(%v) == %#v, want %#v", tt.in, got, tt.want) 224 | } 225 | } 226 | } 227 | 228 | func TestClient_Expr_fail(t *testing.T) { 229 | tests := []struct { 230 | in string 231 | }{ 232 | {in: ""}, 233 | {in: "1 + {}"}, 234 | {in: "xxx"}, 235 | {in: "{x -> x * x}"}, 236 | {in: "function('tr')"}, 237 | } 238 | 239 | for _, tt := range tests { 240 | got, err := cli.Expr(tt.in) 241 | if err != nil { 242 | if err != vim.ErrExpr { 243 | t.Errorf("cli.Expr(%v) want vim.ErrExpr, got %v", tt.in, err) 244 | } 245 | } else { 246 | t.Errorf("cli.Expr(%v) expects error but got %v", tt.in, got) 247 | } 248 | } 249 | } 250 | 251 | func TestClient_Call(t *testing.T) { 252 | tmp, err := ioutil.TempFile("", "vim-go-client-test-log") 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | defer tmp.Close() 257 | defer os.Remove(tmp.Name()) 258 | cli.Expr(fmt.Sprintf("ch_logfile('%s', 'w')", tmp.Name())) 259 | cli.Call("eval", `"1"`) 260 | waitLog() 261 | if !containsString(tmp, ": Calling") { 262 | t.Error(`cli.Expr() should call func`) 263 | } 264 | } 265 | 266 | func TestClient_Call_resp(t *testing.T) { 267 | got, err := cli.Call("eval", `v:true`) 268 | if err != nil { 269 | t.Fatal(err) 270 | } 271 | if !reflect.DeepEqual(got, true) { 272 | t.Error("cli.Call() should return responses") 273 | } 274 | } 275 | 276 | // containsString checks reader contains str. It doens't handle NL and consumes reader! 277 | func containsString(r io.Reader, str string) bool { 278 | scanner := bufio.NewScanner(r) 279 | for scanner.Scan() { 280 | if strings.Contains(scanner.Text(), str) { 281 | return true 282 | } 283 | } 284 | return false 285 | } 286 | -------------------------------------------------------------------------------- /vimserver.go: -------------------------------------------------------------------------------- 1 | package vim 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "text/template" 9 | 10 | "github.com/kr/pty" 11 | ) 12 | 13 | // Process represents Vim server process. 14 | type Process struct { 15 | cmd *exec.Cmd 16 | 17 | script *os.File 18 | } 19 | 20 | // Close closes Vim process. 21 | func (p *Process) Close() error { 22 | os.Remove(p.script.Name()) 23 | return p.cmd.Process.Signal(os.Interrupt) 24 | } 25 | 26 | // send [0, "init connection"] to go server to get initial connection. 27 | const connectScript = ` 28 | call ch_logfile('/tmp/vimchannellog', 'w') 29 | while 1 30 | let g:vim_go_client_handler = ch_open('{{ .Addr }}') 31 | if ch_status(g:vim_go_client_handler) is# 'open' 32 | call ch_sendraw(g:vim_go_client_handler, "[0, \"init connection\"]\n") 33 | break 34 | endif 35 | sleep 50ms 36 | endwhile 37 | ` 38 | 39 | var connectTemplate *template.Template 40 | 41 | func init() { 42 | connectTemplate = template.Must(template.New("connect").Parse(connectScript)) 43 | } 44 | 45 | // NewChildVimServer creates Vim server process and connect to go-server by addr. 46 | func NewChildVimServer(addr string, args []string) (*Process, error) { 47 | tmpfile, err := connectTmpFile(addr) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | cmd, err := vimServerCmd(append([]string{"-S", tmpfile.Name()}, args...)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Emulate terminal to avoid "Input is not from a terminal" 58 | if _, err := pty.Start(cmd); err != nil { 59 | return nil, err 60 | } 61 | 62 | process := &Process{cmd: cmd, script: tmpfile} 63 | 64 | return process, nil 65 | } 66 | 67 | func vimServerCmd(extraArgs []string) (*exec.Cmd, error) { 68 | 69 | path, err := exec.LookPath("vim") 70 | if err != nil { 71 | return nil, fmt.Errorf("vim not found: %v", err) 72 | } 73 | 74 | cmd := &exec.Cmd{ 75 | Path: path, 76 | Args: append([]string{path}, extraArgs...), 77 | } 78 | return cmd, nil 79 | } 80 | 81 | func connectTmpFile(addr string) (*os.File, error) { 82 | tmpfile, err := ioutil.TempFile("", "vim-go-client") 83 | if err != nil { 84 | return nil, err 85 | } 86 | defer tmpfile.Close() 87 | 88 | if err := connectTemplate.Execute(tmpfile, struct{ Addr string }{addr}); err != nil { 89 | return nil, err 90 | } 91 | return tmpfile, nil 92 | } 93 | --------------------------------------------------------------------------------