Skip to content

Commit

Permalink
cmd/govim: temporary implementation of file watching
Browse files Browse the repository at this point in the history
For now use textDocument/didOpen and textDocument/didChange to deal with
generated files changing, whilst we await a proper implementation in
gopls via golang/go#31553.
  • Loading branch information
myitcv committed Apr 30, 2019
1 parent 0391f63 commit a222541
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 24 deletions.
118 changes: 108 additions & 10 deletions cmd/govim/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"
"time"

"github.com/fsnotify/fsnotify"
"github.com/kr/pretty"
"github.com/myitcv/govim"
"github.com/myitcv/govim/cmd/govim/config"
Expand All @@ -30,6 +31,10 @@ type vimstate struct {
// or autocommand.
buffers map[int]*types.Buffer

// watchedFiles is a map of files that we are handling via file watching
// events, rather than via open Buffers in Vim
watchedFiles map[string]*types.WatchedFile

// diagnostics gives us the current diagnostics by URI
diagnostics map[span.URI][]protocol.Diagnostic
diagnosticsChanged bool
Expand Down Expand Up @@ -94,24 +99,24 @@ func (v *vimstate) balloonExpr(args ...json.RawMessage) (interface{}, error) {
}

func (v *vimstate) bufReadPost(args ...json.RawMessage) error {
b, err := v.currentBufferInfo(args[0])
if err != nil {
return err
}
b := v.currentBufferInfo(args[0])
if cb, ok := v.buffers[b.Num]; ok {
// reload of buffer, e.v. e!
b.Version = cb.Version + 1
} else if wf, ok := v.watchedFiles[b.Name]; ok {
// We are now picking up from a file that was previously watched. If we subsequently
// close this buffer then we will handle that event and delete the entry in v.buffers
// at which point the file watching will take back over again.
delete(v.watchedFiles, b.Name)
b.Version = wf.Version + 1
} else {
b.Version = 0
}
return v.handleBufferEvent(b)
}

func (v *vimstate) bufTextChanged(args ...json.RawMessage) error {
b, err := v.currentBufferInfo(args[0])
if err != nil {
return err
}
b := v.currentBufferInfo(args[0])
cb, ok := v.buffers[b.Num]
if !ok {
return fmt.Errorf("have not seen buffer %v (%v) - this should be impossible", b.Num, b.Name)
Expand Down Expand Up @@ -542,9 +547,102 @@ func (v *vimstate) updateQuickfix(args ...json.RawMessage) error {

func (v *vimstate) deleteCurrentBuffer(args ...json.RawMessage) error {
currBufNr := v.ParseInt(args[0])
if _, ok := v.buffers[currBufNr]; !ok {
cb, ok := v.buffers[currBufNr]
if !ok {
return fmt.Errorf("tried to remove buffer %v; but we have no record of it", currBufNr)
}
delete(v.buffers, currBufNr)
delete(v.buffers, cb.Num)
params := &protocol.DidCloseTextDocumentParams{
TextDocument: cb.ToTextDocumentIdentifier(),
}
if err := v.server.DidClose(context.Background(), params); err != nil {
return fmt.Errorf("failed to call gopls.DidClose on %v: %v", cb.Name, err)
}
return nil
}

func (v *vimstate) handleEvent(event fsnotify.Event) error {
// We are handling a filesystem event... so the best we can do is log errors
errf := func(format string, args ...interface{}) {
v.Logf("**** handleEvent error: "+format, args...)
}

path := event.Name

for _, b := range v.buffers {
if b.Name == path {
// Vim is handling this file, do nothing
v.Logf("handleEvent: Vim is in charge of %v; not handling ", event.Name)
return nil
}
}

switch event.Op {
case fsnotify.Rename, fsnotify.Remove:
if _, ok := v.watchedFiles[path]; !ok {
// We saw the Rename/Remove event but nothing before
return nil
}
params := &protocol.DidCloseTextDocumentParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: string(span.URI(path)),
},
}
err := v.server.DidClose(context.Background(), params)
if err != nil {
errf("failed to call server.DidClose: %v", err)
}
return nil
case fsnotify.Create, fsnotify.Chmod, fsnotify.Write:
byts, err := ioutil.ReadFile(path)
if err != nil {
errf("failed to read %v: %v", path, err)
return nil
}
wf, ok := v.watchedFiles[path]
if !ok {
wf = &types.WatchedFile{
Path: path,
Contents: byts,
}
v.watchedFiles[path] = wf
params := &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: string(wf.URI()),
Version: float64(0),
Text: string(wf.Contents),
},
}
err := v.server.DidOpen(context.Background(), params)
if err != nil {
errf("failed to call server.DidOpen: %v", err)
}
v.Logf("handleEvent: handled %v", event)
return nil
}
wf.Version++
params := &protocol.DidChangeTextDocumentParams{
TextDocument: protocol.VersionedTextDocumentIdentifier{
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
URI: string(wf.URI()),
},
Version: float64(wf.Version),
},
ContentChanges: []protocol.TextDocumentContentChangeEvent{
{
Text: string(byts),
},
},
}
err = v.server.DidChange(context.Background(), params)
if err != nil {
errf("failed to call server.DidChange: %v", err)
}
v.Logf("handleEvent: handled %v", event)
return nil

default:
panic(fmt.Errorf("unknown fsnotify event type: %v", event))
}

}
2 changes: 1 addition & 1 deletion cmd/govim/gopls_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func (l loggingGoplsServer) ColorPresentation(ctxt context.Context, params *prot
func (l loggingGoplsServer) Formatting(ctxt context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
l.Logf("gopls.Formatting() call; params:\n%v", pretty.Sprint(params))
res, err := l.u.Formatting(ctxt, params)
l.Logf("gopls.Formatting() return; err: %v; res\n", err, pretty.Sprint(res))
l.Logf("gopls.Formatting() return; err: %v; res:\n%v\n", err, pretty.Sprint(res))
return res, err
}

Expand Down
45 changes: 41 additions & 4 deletions cmd/govim/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net"
"os"
"os/exec"
"strings"
"time"

"github.com/myitcv/govim"
Expand Down Expand Up @@ -117,6 +118,8 @@ type govimplugin struct {
isGui bool

tomb tomb.Tomb

modWatcher *modWatcher
}

func newplugin(goplspath string) *govimplugin {
Expand All @@ -125,9 +128,10 @@ func newplugin(goplspath string) *govimplugin {
goplspath: goplspath,
Driver: d,
vimstate: &vimstate{
Driver: d,
buffers: make(map[int]*types.Buffer),
diagnostics: make(map[span.URI][]protocol.Diagnostic),
Driver: d,
buffers: make(map[int]*types.Buffer),
watchedFiles: make(map[string]*types.WatchedFile),
diagnostics: make(map[span.URI][]protocol.Diagnostic),
},
}
res.vimstate.govimplugin = res
Expand Down Expand Up @@ -210,9 +214,42 @@ func (g *govimplugin) Init(gg govim.Govim, errCh chan error) error {
return fmt.Errorf("failed to initialise gopls: %v", err)
}

// Temporary fix for the fact that gopls does not yet support watching (via
// the client) changed files: https://github.com/golang/go/issues/31553
gomodpath, err := goModPath(wd)
if err != nil {
return fmt.Errorf("failed to derive go.mod path: %v", err)
}

if gomodpath != "" {
// i.e. we are in a module
mw, err := newModWatcher(g, gomodpath)
if err != nil {
return fmt.Errorf("failed to create modWatcher for %v: %v", gomodpath, err)
}
g.modWatcher = mw
}

return nil
}

func (s *govimplugin) Shutdown() error {
func goModPath(wd string) (string, error) {
cmd := exec.Command("go", "env", "GOMOD")
cmd.Dir = wd

out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to execute [%v] in %v: %v\n%s", strings.Join(cmd.Args, " "), wd, err, out)
}

return strings.TrimSpace(string(out)), nil
}

func (g *govimplugin) Shutdown() error {
if g.modWatcher != nil {
if err := g.modWatcher.close(); err != nil {
return err
}
}
return nil
}
6 changes: 6 additions & 0 deletions cmd/govim/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ func TestScripts(t *testing.T) {
"sleep": testdriver.Sleep,
},
Setup: func(e *testscript.Env) error {
// We set a special TMPDIR so the file watcher ignores it
tmp := filepath.Join(e.WorkDir, "_tmp")
if err := os.MkdirAll(tmp, 0777); err != nil {
return fmt.Errorf("failed to create temp dir %v: %v", tmp, err)
}
home := filepath.Join(e.WorkDir, "home")
e.Vars = append(e.Vars,
"TMPDIR="+tmp,
"HOME="+home,
"PLUGIN_PATH="+govimPath,
"CURRENT_GOPATH="+os.Getenv("GOPATH"),
Expand Down
36 changes: 36 additions & 0 deletions cmd/govim/testdata/complete_watched.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Test that ominfunc complete works where the completion is made
# available in a file that is not loaded via the editor.

vim ex 'e main.go'
cp const.go.orig const.go
vim ex 'call cursor(6,16)'
vim ex 'call feedkeys(\"i\\<C-X>\\<C-O>\\<C-N>\\<C-N>\\<ESC>\", \"x\")'
vim ex 'w'
cmp main.go main.go.golden

-- go.mod --
module mod.com

-- main.go --
package main

import "fmt"

func main() {
fmt.Println()
}
-- const.go.orig --
package main

const (
Const1 = 1
Const2 = 2
)
-- main.go.golden --
package main

import "fmt"

func main() {
fmt.Println(Const2)
}
12 changes: 12 additions & 0 deletions cmd/govim/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ type Buffer struct {
cc *span.TokenConverter
}

// A WatchedFile is a file we are watching but that is not loaded as a buffer
// in Vim
type WatchedFile struct {
Path string
Version int
Contents []byte
}

func (w *WatchedFile) URI() span.URI {
return span.FileURI(w.Path)
}

// URI returns the b's Name as a span.URI, assuming it is a file.
// TODO we should panic here is this is not a file-based buffer
func (b *Buffer) URI() span.URI {
Expand Down
13 changes: 4 additions & 9 deletions cmd/govim/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,19 @@ const (

// currentBufferInfo is a helper function to unmarshal autocmd current
// buffer details from expr
func (v *vimstate) currentBufferInfo(expr json.RawMessage) (*types.Buffer, error) {
func (v *vimstate) currentBufferInfo(expr json.RawMessage) *types.Buffer {
var buf struct {
Num int
Name string
Contents string
}
if err := json.Unmarshal(expr, &buf); err != nil {
return nil, fmt.Errorf("failed to unmarshal current buffer info: %v", err)
}
v.Parse(expr, &buf)
res := &types.Buffer{
Num: buf.Num,
Name: buf.Name,
Contents: []byte(buf.Contents),
}
return res, nil
return res
}

func (v *vimstate) cursorPos() (b *types.Buffer, p types.Point, err error) {
Expand All @@ -37,10 +35,7 @@ func (v *vimstate) cursorPos() (b *types.Buffer, p types.Point, err error) {
Col int `json:"col"`
}
expr := v.ChannelExpr(`{"bufnum": bufnr(""), "line": line("."), "col": col(".")}`)
if err = json.Unmarshal(expr, &pos); err != nil {
err = fmt.Errorf("failed to unmarshal current cursor position info: %v", err)
return
}
v.Parse(expr, &pos)
b, ok := v.buffers[pos.BufNum]
if !ok {
err = fmt.Errorf("failed to resolve buffer %v", pos.BufNum)
Expand Down
Loading

0 comments on commit a222541

Please sign in to comment.