From fdb3313e90324c399806313a97d6f00ff94640ed Mon Sep 17 00:00:00 2001 From: Elizabeth Worstell Date: Thu, 21 Mar 2024 13:31:22 -0700 Subject: [PATCH 1/6] feat: add LSP for FTL Add an LSP that produces document highlights when FTL outputs build errors this is a work in progress --- buildengine/engine.go | 10 +- cmd/ftl/cmd_dev.go | 8 +- go.mod | 15 ++ go.sum | 31 ++++ .../scaffolding/{{ .Name | lower }}/pom.xml | 4 +- lsp/logger.go | 67 ++++++++ lsp/lsp.go | 153 ++++++++++++++++++ 7 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 lsp/logger.go create mode 100644 lsp/lsp.go diff --git a/buildengine/engine.go b/buildengine/engine.go index 0ae4c93ae0..2d49bee904 100644 --- a/buildengine/engine.go +++ b/buildengine/engine.go @@ -22,6 +22,7 @@ import ( "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/rpc" + "github.com/TBD54566975/ftl/lsp" ) type schemaChange struct { @@ -190,22 +191,23 @@ func (e *Engine) Deploy(ctx context.Context, replicas int32, waitForDeployOnline } // Dev builds and deploys all local modules and watches for changes, redeploying as necessary. -func (e *Engine) Dev(ctx context.Context, period time.Duration) error { +func (e *Engine) Dev(ctx context.Context, period time.Duration, languageServer *lsp.Server) error { logger := log.FromContext(ctx) // Build and deploy all modules first. err := e.buildAndDeploy(ctx, 1, true) if err != nil { logger.Errorf(err, "initial deploy failed") + languageServer.Post(err) } logger.Infof("All modules deployed, watching for changes...") // Then watch for changes and redeploy. - return e.watchForModuleChanges(ctx, period) + return e.watchForModuleChanges(ctx, period, languageServer) } -func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration) error { +func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration, languageServer *lsp.Server) error { logger := log.FromContext(ctx) moduleHashes := map[string][]byte{} @@ -243,6 +245,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration err := e.buildAndDeploy(ctx, 1, true, config.Key) if err != nil { logger.Errorf(err, "deploy %s failed", config.Key) + languageServer.Post(err) } } case WatchEventProjectRemoved: @@ -258,6 +261,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration config := event.Project.Config() err := e.buildAndDeploy(ctx, 1, true, config.Key) if err != nil { + languageServer.Post(err) switch project := event.Project.(type) { case Module: logger.Errorf(err, "build and deploy failed for module %q: %v", project.Config().Key, err) diff --git a/cmd/ftl/cmd_dev.go b/cmd/ftl/cmd_dev.go index 6c59b346eb..35c0c9346c 100644 --- a/cmd/ftl/cmd_dev.go +++ b/cmd/ftl/cmd_dev.go @@ -11,6 +11,7 @@ import ( "github.com/TBD54566975/ftl/buildengine" "github.com/TBD54566975/ftl/common/projectconfig" "github.com/TBD54566975/ftl/internal/rpc" + "github.com/TBD54566975/ftl/lsp" ) type devCmd struct { @@ -50,6 +51,11 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error g.Go(func() error { return d.ServeCmd.Run(ctx) }) } + languageServer := lsp.NewServer(ctx) + g.Go(func() error { + return languageServer.Run() + }) + g.Go(func() error { err := d.ServeCmd.waitForControllerOnline(ctx, client) if err != nil { @@ -60,7 +66,7 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error if err != nil { return err } - return engine.Dev(ctx, d.Watch) + return engine.Dev(ctx, d.Watch, languageServer) }) return g.Wait() diff --git a/go.mod b/go.mod index 4f5919cd95..b22a3179f5 100644 --- a/go.mod +++ b/go.mod @@ -30,11 +30,15 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.20 github.com/otiai10/copy v1.14.0 + github.com/pkg/errors v0.9.1 github.com/radovskyb/watcher v1.0.7 github.com/rs/cors v1.10.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/swaggest/jsonschema-go v0.3.70 github.com/titanous/json5 v1.0.0 + github.com/tliron/commonlog v0.2.17 + github.com/tliron/glsp v0.2.2 + github.com/tliron/kutil v0.3.24 github.com/tmc/langchaingo v0.1.8 github.com/zalando/go-keyring v0.2.4 go.opentelemetry.io/otel v1.24.0 @@ -56,10 +60,21 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect + github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect ) diff --git a/go.sum b/go.sum index de79a044dc..438cf695c3 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/amacneil/dbmate/v2 v2.14.0 h1:H/bFxk5qbq61NrRE008sKPgMGwMseOQPWsL97vFz+7I= github.com/amacneil/dbmate/v2 v2.14.0/go.mod h1:RNc+B8o1VQkvd6O5C5Zqs7Jq6MNOQDvPl048bFIjFQo= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU= github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -87,6 +89,9 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -95,6 +100,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= @@ -125,10 +132,16 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= @@ -137,6 +150,10 @@ github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -149,6 +166,8 @@ github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8t github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -158,10 +177,16 @@ github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= +github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U= +github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -177,6 +202,12 @@ github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s= github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c= +github.com/tliron/commonlog v0.2.17 h1:GFVvzDZbNLkuQfT45IZeWkrR5AyqiX7Du8pWAtFuPTY= +github.com/tliron/commonlog v0.2.17/go.mod h1:J2Hb63/mMjYmkDzd7E+VL9wCHT6NFNSzV/IOjJWMJqc= +github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c= +github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg= +github.com/tliron/kutil v0.3.24 h1:LvaqizF4htpEef9tC0B//sqtvQzEjDu69A4a1HrY+ko= +github.com/tliron/kutil v0.3.24/go.mod h1:2iSIhOnOe1reqczZQy6TauVHhItsq6xRLV2rVBvodpk= github.com/tmc/langchaingo v0.1.8 h1:nrImgh0aWdu3stJTHz80N60WGwPWY8HXCK10gQny7bA= github.com/tmc/langchaingo v0.1.8/go.mod h1:iNBfS9e6jxBKsJSPWnlqNhoVWgdA3D1g5cdFJjbIZNQ= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= diff --git a/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml b/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml index f7c2a56316..679cc39709 100644 --- a/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml +++ b/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 {{ .GroupID }} diff --git a/lsp/logger.go b/lsp/logger.go new file mode 100644 index 0000000000..cd26dc866c --- /dev/null +++ b/lsp/logger.go @@ -0,0 +1,67 @@ +package lsp + +import ( + "fmt" + + "github.com/tliron/commonlog" + + "github.com/TBD54566975/ftl/internal/log" +) + +// GLSPLogger is a custom logger for the language server. +type GLSPLogger struct { + commonlog.Logger +} + +func (l *GLSPLogger) Log(entry log.Entry) { + l.Logger.Log(toGLSPLevel(entry.Level), 10, entry.Message, entry.Attributes) +} + +func (l *GLSPLogger) Logf(level log.Level, format string, args ...interface{}) { + l.Log(log.Entry{Level: level, Message: fmt.Sprintf(format, args...)}) +} +func (l *GLSPLogger) Tracef(format string, args ...interface{}) { + l.Log(log.Entry{Level: log.Trace, Message: fmt.Sprintf(format, args...)}) +} + +func (l *GLSPLogger) Debugf(format string, args ...interface{}) { + l.Log(log.Entry{Level: log.Debug, Message: fmt.Sprintf(format, args...)}) +} + +func (l *GLSPLogger) Infof(format string, args ...interface{}) { + l.Log(log.Entry{Level: log.Info, Message: fmt.Sprintf(format, args...)}) +} + +func (l *GLSPLogger) Warnf(format string, args ...interface{}) { + l.Log(log.Entry{Level: log.Warn, Message: fmt.Sprintf(format, args...)}) +} + +func (l *GLSPLogger) Errorf(err error, format string, args ...interface{}) { + if err == nil { + return + } + l.Log(log.Entry{Level: log.Error, Message: fmt.Sprintf(format, args...) + ": " + err.Error(), Error: err}) +} + +var _ log.Interface = (*GLSPLogger)(nil) + +func NewGLSPLogger(log commonlog.Logger) *GLSPLogger { + return &GLSPLogger{log} +} + +func toGLSPLevel(l log.Level) commonlog.Level { + switch l { + case log.Trace: + return commonlog.Debug + case log.Debug: + return commonlog.Debug + case log.Info: + return commonlog.Info + case log.Warn: + return commonlog.Warning + case log.Error: + return commonlog.Error + default: + return commonlog.Debug + } +} diff --git a/lsp/lsp.go b/lsp/lsp.go new file mode 100644 index 0000000000..7ac1a755ec --- /dev/null +++ b/lsp/lsp.go @@ -0,0 +1,153 @@ +package lsp + +import ( + "context" + "strings" + + "github.com/pkg/errors" + _ "github.com/tliron/commonlog/simple" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" + glspServer "github.com/tliron/glsp/server" + "github.com/tliron/kutil/version" + + "github.com/TBD54566975/ftl/go-runtime/compile" + ftlErrors "github.com/TBD54566975/ftl/internal/errors" +) + +const lsName = "ftl-language-server" + +// Server is a language server. +type Server struct { + Server *glspServer.Server + GlspLogger *GLSPLogger + glspContext *glsp.Context + handler protocol.Handler +} + +// NewServer creates a new language server. +func NewServer(ctx context.Context) *Server { + handler := protocol.Handler{ + Initialized: initialized, + Shutdown: shutdown, + SetTrace: setTrace, + LogTrace: logTrace, + } + s := glspServer.NewServer(&handler, lsName, false) + server := &Server{ + Server: s, + GlspLogger: NewGLSPLogger(s.Log), + } + handler.Initialize = server.initialize() + // handler.TextDocumentDidOpen = server.textDocumentDidOpen() + handler.TextDocumentDidChange = server.textDocumentDidChange() + return server +} + +func (s *Server) Run() error { + return errors.Wrap(s.Server.RunStdio(), "lsp") +} + +type errSet map[string]compile.Error + +func (s *Server) Post(err error) { + errByFilename := make(map[string]errSet) + + // Deduplicate and associate by filename. + for _, subErr := range ftlErrors.UnwrapAll(err) { + var ce compile.Error + if errors.As(subErr, &ce) { + cp := ce.Pos + if errByFilename[cp.Filename] == nil { + errByFilename[cp.Filename] = errSet{} + } + errByFilename[cp.Filename][strings.TrimSpace(ce.Error())] = ce + } + } + + go func() { + for filename, errs := range errByFilename { + var diagnostics []protocol.Diagnostic + for _, e := range errs { + pp := e.Pos + sourceName := "ftl" + severity := protocol.DiagnosticSeverityError + diagnostics = append(diagnostics, protocol.Diagnostic{ + Range: protocol.Range{ + Start: protocol.Position{Line: protocol.UInteger(pp.Line), Character: protocol.UInteger(pp.Column + pp.Offset)}, + End: protocol.Position{Line: protocol.UInteger(pp.Line), Character: protocol.UInteger(pp.Column + pp.Offset + 10)}, //todo: fix + }, + Severity: &severity, + Source: &sourceName, + Message: e.Msg, + }) + break + } + + go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ + URI: "file://" + filename, + Diagnostics: diagnostics, + }) + } + }() +} + +func (s *Server) initialize() protocol.InitializeFunc { + return func(context *glsp.Context, params *protocol.InitializeParams) (any, error) { + s.glspContext = context + + if params.Trace != nil { + protocol.SetTraceValue(*params.Trace) + } + + serverCapabilities := s.handler.CreateServerCapabilities() + return protocol.InitializeResult{ + Capabilities: serverCapabilities, + ServerInfo: &protocol.InitializeResultServerInfo{ + Name: lsName, + Version: &version.GitVersion, + }, + }, nil + } +} + +func (s *Server) textDocumentDidOpen() protocol.TextDocumentDidOpenFunc { + return func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error { + // s.refreshDiagnosticsOfDocument(params.TextDocument.URI) + return nil + } +} + +func (s *Server) textDocumentDidChange() protocol.TextDocumentDidChangeFunc { + return func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error { + // s.refreshDiagnosticsOfDocument(params.TextDocument.URI) + return nil + } +} + +func (s *Server) refreshDiagnosticsOfDocument(uri protocol.DocumentUri) { + go func() { + go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ + URI: uri, + Diagnostics: []protocol.Diagnostic{}, + }) + }() +} + +func initialized(context *glsp.Context, params *protocol.InitializedParams) error { + return nil +} + +func shutdown(context *glsp.Context) error { + protocol.SetTraceValue(protocol.TraceValueOff) + return nil +} + +func logTrace(context *glsp.Context, params *protocol.LogTraceParams) error { + return nil +} + +func setTrace(context *glsp.Context, params *protocol.SetTraceParams) error { + protocol.SetTraceValue(params.Value) + return nil +} From 3ec35d72ef326078185a2c12cffc872b91c3430b Mon Sep 17 00:00:00 2001 From: Wes Date: Sat, 30 Mar 2024 11:43:14 -0700 Subject: [PATCH 2/6] working lsp with location in vscode --- .../compile => backend/schema}/errors.go | 22 ++++----- go-runtime/compile/schema.go | 20 +++++--- lsp/lsp.go | 46 +++++++++---------- 3 files changed, 46 insertions(+), 42 deletions(-) rename {go-runtime/compile => backend/schema}/errors.go (50%) diff --git a/go-runtime/compile/errors.go b/backend/schema/errors.go similarity index 50% rename from go-runtime/compile/errors.go rename to backend/schema/errors.go index ab4ef6e1b9..66345d6386 100644 --- a/go-runtime/compile/errors.go +++ b/backend/schema/errors.go @@ -1,41 +1,37 @@ -package compile +package schema import ( "errors" "fmt" - "go/ast" - "go/token" - - "github.com/TBD54566975/ftl/backend/schema" ) type Error struct { Msg string - Pos schema.Position + Pos Position Err error // Wrapped error, if any } func (e Error) Error() string { return fmt.Sprintf("%s: %s", e.Pos, e.Msg) } func (e Error) Unwrap() error { return e.Err } -func errorf(pos token.Pos, format string, args ...any) Error { - return Error{Msg: fmt.Sprintf(format, args...), Pos: goPosToSchemaPos(pos)} +func Errorf(pos Position, format string, args ...any) Error { + return Error{Msg: fmt.Sprintf(format, args...), Pos: pos} } -func wrapf(node ast.Node, err error, format string, args ...any) Error { +func Wrapf(pos Position, err error, format string, args ...any) Error { if format == "" { format = "%s" } else { format += ": %s" } // Propagate existing error position if available - var pos schema.Position + var newPos Position if perr := (Error{}); errors.As(err, &perr) { - pos = perr.Pos + newPos = perr.Pos args = append(args, perr.Msg) } else { - pos = goPosToSchemaPos(node.Pos()) + newPos = pos args = append(args, err) } - return Error{Msg: fmt.Sprintf(format, args...), Pos: pos, Err: err} + return Error{Msg: fmt.Sprintf(format, args...), Pos: newPos, Err: err} } diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index bb21aa4477..6def35d8fd 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -44,6 +44,14 @@ type NativeNames map[schema.Decl]string type enums map[string]*schema.Enum +func errorf(pos token.Pos, format string, args ...interface{}) schema.Error { + return schema.Errorf(goPosToSchemaPos(pos), format, args...) +} + +func wrapf(pos token.Pos, err error, format string, args ...interface{}) schema.Error { + return schema.Wrapf(goPosToSchemaPos(pos), err, format, args...) +} + // ExtractModuleSchema statically parses Go FTL module source into a schema.Module. func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, error) { pkgs, err := packages.Load(&packages.Config{ @@ -70,7 +78,7 @@ func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, error) { if len(pkg.Errors) > 0 { for _, perr := range pkg.Errors { if len(pkg.Syntax) > 0 { - merr = append(merr, wrapf(pkg.Syntax[0], perr, "%s", pkg.PkgPath)) + merr = append(merr, wrapf(pkg.Syntax[0].Pos(), perr, "%s", pkg.PkgPath)) } else { merr = append(merr, fmt.Errorf("%s: %w", pkg.PkgPath, perr)) } @@ -81,7 +89,7 @@ func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, error) { err := goast.Visit(file, func(node ast.Node, next func() error) (err error) { defer func() { if err != nil { - err = wrapf(node, err, "") + err = wrapf(node.Pos(), err, "") } }() switch node := node.(type) { @@ -188,7 +196,7 @@ func parseConfigDecl(pctx *parseContext, node *ast.CallExpr, fn *types.Func) err var err error name, err = strconv.Unquote(literal.Value) if err != nil { - return wrapf(node, err, "") + return wrapf(node.Pos(), err, "") } } } @@ -445,7 +453,7 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb, e results := sig.Results() reqt, respt, err := checkSignature(sig) if err != nil { - return nil, wrapf(node, err, "") + return nil, wrapf(node.Pos(), err, "") } var req schema.Type if reqt != nil { @@ -602,7 +610,7 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) (*schema.R f := s.Field(i) ft, err := visitType(pctx, f.Pos(), f.Type()) if err != nil { - return nil, errorf(pos, "field %s: %v", f.Name(), err) + return nil, wrapf(pos, err, "field %s", f.Name()) } // Check if field is exported @@ -746,7 +754,7 @@ func visitType(pctx *parseContext, pos token.Pos, tnode types.Type) (schema.Type if underlying.String() == "any" { return &schema.Any{Pos: goPosToSchemaPos(pos)}, nil } - return nil, errorf(pos, "unsupported type %T", tnode) + return nil, errorf(pos, "unsupported type %q", tnode) default: return nil, errorf(pos, "unsupported type %T", tnode) diff --git a/lsp/lsp.go b/lsp/lsp.go index 7ac1a755ec..9eaa51fd05 100644 --- a/lsp/lsp.go +++ b/lsp/lsp.go @@ -11,18 +11,20 @@ import ( glspServer "github.com/tliron/glsp/server" "github.com/tliron/kutil/version" - "github.com/TBD54566975/ftl/go-runtime/compile" + "github.com/TBD54566975/ftl/backend/schema" ftlErrors "github.com/TBD54566975/ftl/internal/errors" + "github.com/TBD54566975/ftl/internal/log" ) const lsName = "ftl-language-server" // Server is a language server. type Server struct { - Server *glspServer.Server - GlspLogger *GLSPLogger + server *glspServer.Server + glspLogger *GLSPLogger glspContext *glsp.Context handler protocol.Handler + logger log.Logger } // NewServer creates a new language server. @@ -35,8 +37,9 @@ func NewServer(ctx context.Context) *Server { } s := glspServer.NewServer(&handler, lsName, false) server := &Server{ - Server: s, - GlspLogger: NewGLSPLogger(s.Log), + server: s, + glspLogger: NewGLSPLogger(s.Log), + logger: *log.FromContext(ctx), } handler.Initialize = server.initialize() // handler.TextDocumentDidOpen = server.textDocumentDidOpen() @@ -45,23 +48,24 @@ func NewServer(ctx context.Context) *Server { } func (s *Server) Run() error { - return errors.Wrap(s.Server.RunStdio(), "lsp") + return errors.Wrap(s.server.RunStdio(), "lsp") } -type errSet map[string]compile.Error +type errSet map[string]schema.Error +// Post sends diagnostics to the client. err must be joined schema.Errors. func (s *Server) Post(err error) { errByFilename := make(map[string]errSet) // Deduplicate and associate by filename. for _, subErr := range ftlErrors.UnwrapAll(err) { - var ce compile.Error + var ce schema.Error if errors.As(subErr, &ce) { - cp := ce.Pos - if errByFilename[cp.Filename] == nil { - errByFilename[cp.Filename] = errSet{} + filename := ce.Pos.Filename + if _, exists := errByFilename[filename]; !exists { + errByFilename[filename] = make(errSet) } - errByFilename[cp.Filename][strings.TrimSpace(ce.Error())] = ce + errByFilename[filename][strings.TrimSpace(ce.Error())] = ce } } @@ -74,14 +78,17 @@ func (s *Server) Post(err error) { severity := protocol.DiagnosticSeverityError diagnostics = append(diagnostics, protocol.Diagnostic{ Range: protocol.Range{ - Start: protocol.Position{Line: protocol.UInteger(pp.Line), Character: protocol.UInteger(pp.Column + pp.Offset)}, - End: protocol.Position{Line: protocol.UInteger(pp.Line), Character: protocol.UInteger(pp.Column + pp.Offset + 10)}, //todo: fix + Start: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column - 1)}, + End: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column + 10 - 1)}, //todo: fix }, Severity: &severity, Source: &sourceName, Message: e.Msg, }) - break + } + + if s.glspContext == nil { + return } go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ @@ -111,13 +118,6 @@ func (s *Server) initialize() protocol.InitializeFunc { } } -func (s *Server) textDocumentDidOpen() protocol.TextDocumentDidOpenFunc { - return func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error { - // s.refreshDiagnosticsOfDocument(params.TextDocument.URI) - return nil - } -} - func (s *Server) textDocumentDidChange() protocol.TextDocumentDidChangeFunc { return func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error { // s.refreshDiagnosticsOfDocument(params.TextDocument.URI) @@ -125,7 +125,7 @@ func (s *Server) textDocumentDidChange() protocol.TextDocumentDidChangeFunc { } } -func (s *Server) refreshDiagnosticsOfDocument(uri protocol.DocumentUri) { +func (s *Server) clearDiagnosticsOfDocument(uri protocol.DocumentUri) { go func() { go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ URI: uri, From ce4d9d36c54a9daa904d6fe76704b2b4c3ddd5e6 Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 1 Apr 2024 11:56:01 -0700 Subject: [PATCH 3/6] Make lsp option on cmd_dev --- cmd/ftl/cmd_dev.go | 9 ++++--- lsp/lsp.go | 61 ++++++++++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/cmd/ftl/cmd_dev.go b/cmd/ftl/cmd_dev.go index 35c0c9346c..d0fef74cd9 100644 --- a/cmd/ftl/cmd_dev.go +++ b/cmd/ftl/cmd_dev.go @@ -20,6 +20,7 @@ type devCmd struct { External []string `help:"Directories for libraries that require FTL module stubs." type:"existingdir" optional:""` Watch time.Duration `help:"Watch template directory at this frequency and regenerate on change." default:"500ms"` NoServe bool `help:"Do not start the FTL server." default:"false"` + RunLsp bool `help:"Run the language server." default:"false"` ServeCmd serveCmd `embed:""` } @@ -52,9 +53,11 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error } languageServer := lsp.NewServer(ctx) - g.Go(func() error { - return languageServer.Run() - }) + if d.RunLsp { + g.Go(func() error { + return languageServer.Run() + }) + } g.Go(func() error { err := d.ServeCmd.waitForControllerOnline(ctx, client) diff --git a/lsp/lsp.go b/lsp/lsp.go index 9eaa51fd05..404f0bbb7b 100644 --- a/lsp/lsp.go +++ b/lsp/lsp.go @@ -2,9 +2,10 @@ package lsp import ( "context" + "errors" + "fmt" "strings" - "github.com/pkg/errors" _ "github.com/tliron/commonlog/simple" "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" @@ -48,7 +49,11 @@ func NewServer(ctx context.Context) *Server { } func (s *Server) Run() error { - return errors.Wrap(s.server.RunStdio(), "lsp") + err := s.server.RunStdio() + if err != nil { + return fmt.Errorf("lsp: %w", err) + } + return nil } type errSet map[string]schema.Error @@ -69,34 +74,36 @@ func (s *Server) Post(err error) { } } - go func() { - for filename, errs := range errByFilename { - var diagnostics []protocol.Diagnostic - for _, e := range errs { - pp := e.Pos - sourceName := "ftl" - severity := protocol.DiagnosticSeverityError - diagnostics = append(diagnostics, protocol.Diagnostic{ - Range: protocol.Range{ - Start: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column - 1)}, - End: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column + 10 - 1)}, //todo: fix - }, - Severity: &severity, - Source: &sourceName, - Message: e.Msg, - }) - } - - if s.glspContext == nil { - return - } + go publishErrors(errByFilename, s) +} - go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ - URI: "file://" + filename, - Diagnostics: diagnostics, +func publishErrors(errByFilename map[string]errSet, s *Server) { + for filename, errs := range errByFilename { + var diagnostics []protocol.Diagnostic + for _, e := range errs { + pp := e.Pos + sourceName := "ftl" + severity := protocol.DiagnosticSeverityError + diagnostics = append(diagnostics, protocol.Diagnostic{ + Range: protocol.Range{ + Start: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column - 1)}, + End: protocol.Position{Line: uint32(pp.Line - 1), Character: uint32(pp.Column + 10 - 1)}, + }, + Severity: &severity, + Source: &sourceName, + Message: e.Msg, }) } - }() + + if s.glspContext == nil { + return + } + + go s.glspContext.Notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ + URI: "file://" + filename, + Diagnostics: diagnostics, + }) + } } func (s *Server) initialize() protocol.InitializeFunc { From 17e180dc797055b1e90790f760a563ec3aba8885 Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 2 Apr 2024 10:27:34 -0700 Subject: [PATCH 4/6] Use listener for engine events --- buildengine/engine.go | 33 ++++++++++++++++++++++--------- cmd/ftl/cmd_dev.go | 46 ++++++++++++++++++++++++++++--------------- lsp/lsp.go | 15 +++++++++++++- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/buildengine/engine.go b/buildengine/engine.go index 2d49bee904..d0e147df04 100644 --- a/buildengine/engine.go +++ b/buildengine/engine.go @@ -22,7 +22,6 @@ import ( "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/rpc" - "github.com/TBD54566975/ftl/lsp" ) type schemaChange struct { @@ -30,6 +29,11 @@ type schemaChange struct { *schema.Module } +type Listener struct { + OnBuildComplete func(project Project, err error) + OnDeployComplete func(project Project, err error) +} + // Engine for building a set of modules. type Engine struct { client ftlv1connect.ControllerServiceClient @@ -40,6 +44,7 @@ type Engine struct { schemaChanges *pubsub.Topic[schemaChange] cancel func() parallelism int + listener *Listener } type Option func(o *Engine) @@ -50,6 +55,13 @@ func Parallelism(n int) Option { } } +// WithListener sets the event listener for the Engine. +func WithListener(listener *Listener) Option { + return func(o *Engine) { + o.listener = listener + } +} + // New constructs a new [Engine]. // // Completely offline builds are possible if the full dependency graph is @@ -191,23 +203,22 @@ func (e *Engine) Deploy(ctx context.Context, replicas int32, waitForDeployOnline } // Dev builds and deploys all local modules and watches for changes, redeploying as necessary. -func (e *Engine) Dev(ctx context.Context, period time.Duration, languageServer *lsp.Server) error { +func (e *Engine) Dev(ctx context.Context, period time.Duration) error { logger := log.FromContext(ctx) // Build and deploy all modules first. err := e.buildAndDeploy(ctx, 1, true) if err != nil { logger.Errorf(err, "initial deploy failed") - languageServer.Post(err) } logger.Infof("All modules deployed, watching for changes...") // Then watch for changes and redeploy. - return e.watchForModuleChanges(ctx, period, languageServer) + return e.watchForModuleChanges(ctx, period) } -func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration, languageServer *lsp.Server) error { +func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration) error { logger := log.FromContext(ctx) moduleHashes := map[string][]byte{} @@ -245,7 +256,6 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration err := e.buildAndDeploy(ctx, 1, true, config.Key) if err != nil { logger.Errorf(err, "deploy %s failed", config.Key) - languageServer.Post(err) } } case WatchEventProjectRemoved: @@ -261,7 +271,6 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration config := event.Project.Config() err := e.buildAndDeploy(ctx, 1, true, config.Key) if err != nil { - languageServer.Post(err) switch project := event.Project.(type) { case Module: logger.Errorf(err, "build and deploy failed for module %q: %v", project.Config().Key, err) @@ -360,7 +369,11 @@ func (e *Engine) buildAndDeploy(ctx context.Context, replicas int32, waitForDepl return nil } if module, ok := project.(Module); ok { - if err := Deploy(ctx, module, replicas, waitForDeployOnline, e.client); err != nil { + err := Deploy(ctx, module, replicas, waitForDeployOnline, e.client) + if e.listener != nil && e.listener.OnDeployComplete != nil { + e.listener.OnDeployComplete(project, err) + } + if err != nil { return err } } @@ -461,8 +474,10 @@ func (e *Engine) build(ctx context.Context, key ProjectKey, builtModules map[str sch := &schema.Schema{Modules: maps.Values(combined)} err := Build(ctx, sch, project) + if e.listener != nil && e.listener.OnBuildComplete != nil { + e.listener.OnBuildComplete(project, err) + } if err != nil { - return err } if module, ok := project.(Module); ok { diff --git a/cmd/ftl/cmd_dev.go b/cmd/ftl/cmd_dev.go index d0fef74cd9..d0236e02b8 100644 --- a/cmd/ftl/cmd_dev.go +++ b/cmd/ftl/cmd_dev.go @@ -15,13 +15,14 @@ import ( ) type devCmd struct { - Parallelism int `short:"j" help:"Number of modules to build in parallel." default:"${numcpu}"` - Dirs []string `arg:"" help:"Base directories containing modules." type:"existingdir" optional:""` - External []string `help:"Directories for libraries that require FTL module stubs." type:"existingdir" optional:""` - Watch time.Duration `help:"Watch template directory at this frequency and regenerate on change." default:"500ms"` - NoServe bool `help:"Do not start the FTL server." default:"false"` - RunLsp bool `help:"Run the language server." default:"false"` - ServeCmd serveCmd `embed:""` + Parallelism int `short:"j" help:"Number of modules to build in parallel." default:"${numcpu}"` + Dirs []string `arg:"" help:"Base directories containing modules." type:"existingdir" optional:""` + External []string `help:"Directories for libraries that require FTL module stubs." type:"existingdir" optional:""` + Watch time.Duration `help:"Watch template directory at this frequency and regenerate on change." default:"500ms"` + NoServe bool `help:"Do not start the FTL server." default:"false"` + RunLsp bool `help:"Run the language server." default:"false"` + ServeCmd serveCmd `embed:""` + languageServer *lsp.Server } func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error { @@ -52,25 +53,38 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error g.Go(func() error { return d.ServeCmd.Run(ctx) }) } - languageServer := lsp.NewServer(ctx) - if d.RunLsp { - g.Go(func() error { - return languageServer.Run() - }) - } - g.Go(func() error { err := d.ServeCmd.waitForControllerOnline(ctx, client) if err != nil { return err } - engine, err := buildengine.New(ctx, client, d.Dirs, d.External, buildengine.Parallelism(d.Parallelism)) + var listener *buildengine.Listener + if d.RunLsp { + d.languageServer = lsp.NewServer(ctx) + listener = &buildengine.Listener{ + OnBuildComplete: d.OnBuildComplete, + OnDeployComplete: d.OnDeployComplete, + } + g.Go(func() error { + return d.languageServer.Run() + }) + } + + engine, err := buildengine.New(ctx, client, d.Dirs, d.External, buildengine.Parallelism(d.Parallelism), buildengine.WithListener(listener)) if err != nil { return err } - return engine.Dev(ctx, d.Watch, languageServer) + return engine.Dev(ctx, d.Watch) }) return g.Wait() } + +func (d *devCmd) OnBuildComplete(project buildengine.Project, err error) { + d.languageServer.BuildComplete(project.Config().Dir, err) +} + +func (d *devCmd) OnDeployComplete(project buildengine.Project, err error) { + d.languageServer.DeployComplete(project.Config().Dir, err) +} diff --git a/lsp/lsp.go b/lsp/lsp.go index 404f0bbb7b..99183a0dd7 100644 --- a/lsp/lsp.go +++ b/lsp/lsp.go @@ -58,12 +58,25 @@ func (s *Server) Run() error { type errSet map[string]schema.Error +func (s *Server) BuildComplete(dir string, err error) { + if err != nil { + s.post(err) + } +} + +func (s *Server) DeployComplete(dir string, err error) { + if err != nil { + s.post(err) + } +} + // Post sends diagnostics to the client. err must be joined schema.Errors. -func (s *Server) Post(err error) { +func (s *Server) post(err error) { errByFilename := make(map[string]errSet) // Deduplicate and associate by filename. for _, subErr := range ftlErrors.UnwrapAll(err) { + //TODO: Need a way to pass structured errors from other runtimes like kotlin. This won't work for them. var ce schema.Error if errors.As(subErr, &ce) { filename := ce.Pos.Filename From 85e16009d59c47589b2efb0e6712704a49f0c2be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Apr 2024 17:42:57 +0000 Subject: [PATCH 5/6] chore(autofmt): Automated formatting --- go.mod | 2 +- kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index b22a3179f5..c6a7847e26 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,6 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.20 github.com/otiai10/copy v1.14.0 - github.com/pkg/errors v0.9.1 github.com/radovskyb/watcher v1.0.7 github.com/rs/cors v1.10.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 @@ -70,6 +69,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect diff --git a/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml b/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml index 679cc39709..f7c2a56316 100644 --- a/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml +++ b/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 {{ .GroupID }} From 2dd7a0b17d9d4d4268557c7e441d262ed11f3200 Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 2 Apr 2024 14:12:27 -0700 Subject: [PATCH 6/6] fix schema error position info --- go-runtime/compile/schema.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index 6def35d8fd..9ab6ee1584 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -610,12 +610,12 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) (*schema.R f := s.Field(i) ft, err := visitType(pctx, f.Pos(), f.Type()) if err != nil { - return nil, wrapf(pos, err, "field %s", f.Name()) + return nil, errorf(f.Pos(), "field %s: %v", f.Name(), err) } // Check if field is exported if len(f.Name()) > 0 && unicode.IsLower(rune(f.Name()[0])) { - return nil, errorf(pos, "params field %s must be exported by starting with an uppercase letter", f.Name()) + return nil, errorf(f.Pos(), "params field %s must be exported by starting with an uppercase letter", f.Name()) } // Extract the JSON tag and split it to get just the field name @@ -629,13 +629,13 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) (*schema.R var metadata []schema.Metadata if jsonFieldName != "" { metadata = append(metadata, &schema.MetadataAlias{ - Pos: goPosToSchemaPos(pos), + Pos: goPosToSchemaPos(f.Pos()), Kind: schema.AliasKindJSON, Alias: jsonFieldName, }) } out.Fields = append(out.Fields, &schema.Field{ - Pos: goPosToSchemaPos(pos), + Pos: goPosToSchemaPos(f.Pos()), Name: strcase.ToLowerCamel(f.Name()), Type: ft, Metadata: metadata,