Skip to content

Commit

Permalink
lsp: Load config from parent dirs (#650)
Browse files Browse the repository at this point in the history
* lsp: Load config from parent dirs

Fixes #626

Signed-off-by: Charlie Egan <charlie@styra.com>

* lsp: watcher error, use :=

Signed-off-by: Charlie Egan <charlie@styra.com>

* lsp: Wait in server tests

Server tests refactored a little to allow messages to be sent in a
different order.

Signed-off-by: Charlie Egan <charlie@styra.com>

* lsp: PR review comments

Signed-off-by: Charlie Egan <charlie@styra.com>

* lsp: further reduce the config watch timeout

Signed-off-by: Charlie Egan <charlie@styra.com>

* lsp: server test use shared timeout

Signed-off-by: Charlie Egan <charlie@styra.com>

---------

Signed-off-by: Charlie Egan <charlie@styra.com>
  • Loading branch information
charlieegan3 authored Apr 15, 2024
1 parent fe1dc3a commit c31886e
Show file tree
Hide file tree
Showing 8 changed files with 597 additions and 207 deletions.
1 change: 1 addition & 0 deletions cmd/languageserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func init() {
go ls.StartDiagnosticsWorker(ctx)
go ls.StartHoverWorker(ctx)
go ls.StartCommandWorker(ctx)
go ls.StartConfigWorker(ctx)

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.22.0
require (
dario.cat/mergo v1.0.0
github.com/fatih/color v1.16.0
github.com/fsnotify/fsnotify v1.7.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.6.0
github.com/olekukonko/tablewriter v0.0.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.1.4/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
Expand Down
5 changes: 5 additions & 0 deletions internal/lsp/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ type Identifier int
const (
IdentifierGeneric Identifier = iota
IdentifierVSCode
IdentifierGoTest
)

func DetermineClientIdentifier(clientName string) Identifier {
if clientName == "go test" {
return IdentifierGoTest
}

if clientName == "Visual Studio Code" {
return IdentifierVSCode
}
Expand Down
132 changes: 132 additions & 0 deletions internal/lsp/config/watcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package config

import (
"context"
"fmt"
"io"

"github.com/fsnotify/fsnotify"
)

type Watcher struct {
Reload chan string
Drop chan struct{}

path string
pathUpdates chan string

fsWatcher *fsnotify.Watcher

errorWriter io.Writer
}

type WatcherOpts struct {
ErrorWriter io.Writer
Path string
}

func NewWatcher(opts *WatcherOpts) *Watcher {
w := &Watcher{
Reload: make(chan string, 1),
Drop: make(chan struct{}, 1),
pathUpdates: make(chan string, 1),
}

if opts != nil {
w.errorWriter = opts.ErrorWriter
w.path = opts.Path
}

return w
}

func (w *Watcher) Start(ctx context.Context) error {
err := w.Stop()
if err != nil {
return fmt.Errorf("failed to stop existing watcher: %w", err)
}

w.fsWatcher, err = fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create fsnotify watcher: %w", err)
}

go func() {
w.loop(ctx)
}()

return nil
}

func (w *Watcher) loop(ctx context.Context) {
for {
select {
case path := <-w.pathUpdates:
if w.path != "" {
err := w.fsWatcher.Remove(w.path)
if err != nil {
fmt.Fprintf(w.errorWriter, "failed to remove existing watch: %v\n", err)
}
}

err := w.fsWatcher.Add(path)
if err != nil {
fmt.Fprintf(w.errorWriter, "failed to add watch: %v\n", err)
}

w.path = path

// when the path itself is changed, then this is an event too
w.Reload <- path
case event, ok := <-w.fsWatcher.Events:
if !ok {
fmt.Fprintf(w.errorWriter, "config watcher event channel closed\n")

return
}

if event.Has(fsnotify.Write) {
w.Reload <- event.Name
}

if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
w.path = ""
w.Drop <- struct{}{}
}
case err := <-w.fsWatcher.Errors:
fmt.Fprintf(w.errorWriter, "config watcher error: %v\n", err)
case <-ctx.Done():
err := w.Stop()
if err != nil {
fmt.Fprintf(w.errorWriter, "failed to stop watcher: %v\n", err)
}

return
}
}
}

func (w *Watcher) Watch(configFilePath string) {
w.pathUpdates <- configFilePath
}

func (w *Watcher) Stop() error {
if w.fsWatcher != nil {
err := w.fsWatcher.Close()
if err != nil {
return fmt.Errorf("failed to close fsnotify watcher: %w", err)
}

return nil
}

return nil
}

func (w *Watcher) IsWatching() bool {
if w.fsWatcher == nil {
return false
}

return len(w.fsWatcher.WatchList()) > 0
}
71 changes: 71 additions & 0 deletions internal/lsp/config/watcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package config

import (
"context"
"os"
"testing"
"time"
)

func TestWatcher(t *testing.T) {
t.Parallel()

tempDir := t.TempDir()

configFilePath := tempDir + "/config.yaml"

configFileContents := `---
foo: bar
`

err := os.WriteFile(configFilePath, []byte(configFileContents), 0o600)
if err != nil {
t.Fatal(err)
}

watcher := NewWatcher(&WatcherOpts{ErrorWriter: os.Stderr})

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
err = watcher.Start(ctx)
if err != nil {
t.Errorf("failed to start watcher: %v", err)
}
}()

watcher.Watch(configFilePath)

select {
case <-watcher.Reload:
case <-time.After(100 * time.Millisecond):
t.Fatal("timeout waiting for initial config event")
}

newConfigFileContents := `---
foo: baz
`

err = os.WriteFile(configFilePath, []byte(newConfigFileContents), 0o600)
if err != nil {
t.Fatal(err)
}

select {
case <-watcher.Reload:
case <-time.After(100 * time.Millisecond):
t.Fatal("timeout waiting for config event")
}

err = os.Rename(configFilePath, configFilePath+".new")
if err != nil {
t.Fatal(err)
}

select {
case <-watcher.Drop:
case <-time.After(100 * time.Millisecond):
t.Fatal("timeout waiting for config drop event")
}
}
Loading

0 comments on commit c31886e

Please sign in to comment.