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 d7f80351ee..f8a1bd1cdc 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 { @@ -52,6 +53,11 @@ func (d *devCmd) Run(ctx context.Context, projConfig projectconfig.Config) error }) } + languageServer := lsp.NewServer(ctx) + g.Go(func() error { + return languageServer.Run() + }) + err := d.ServeCmd.pollControllerOnine(ctx, client) if err != nil { return err @@ -62,7 +68,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/examples/go/echo/echo.go b/examples/go/echo/echo.go index 39f3f6fe0e..175eeaca47 100644 --- a/examples/go/echo/echo.go +++ b/examples/go/echo/echo.go @@ -32,16 +32,3 @@ func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{Message: fmt.Sprintf("Hello, %s!!! It is %s!", req.Name.Default(defaultName.Get(ctx)), tresp.Time)}, nil } - -/* - -verb cronJob(Unit) Unit - +cron "0 0 * * *" - -*/ - -//ftl:cron -func CronJob(ctx context.Context) error { - _, err := ftl.Call(ctx, Echo, EchoRequest{}) - return err -} diff --git a/go.mod b/go.mod index e65f3e6aa2..edba92b268 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.69 github.com/titanous/json5 v1.0.0 + github.com/tliron/commonlog v0.2.16 + github.com/tliron/glsp v0.2.2 + github.com/tliron/kutil v0.3.24 github.com/tmc/langchaingo v0.1.5 github.com/zalando/go-keyring v0.2.3 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.2 // 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 afdf1e2ebe..c9575288c0 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,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.12.0 h1:2F/Fu/lScBhsQ8UgPg/UPM4QtBBpieZWntDJYaAkGHo= github.com/amacneil/dbmate/v2 v2.12.0/go.mod h1:D+FLHuUDma3qQyyh691Y/80tiNdoobe0kqaY7TqF0FM= +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= @@ -85,6 +87,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= @@ -93,6 +98,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= @@ -123,10 +130,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= @@ -135,6 +148,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.2 h1:u7PCSBiWJ3nJYoTGShyM9iHXz4dNyYkurwwp+GHtyHY= github.com/pkoukk/tiktoken-go v0.1.2/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -147,6 +164,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= @@ -156,10 +175,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= @@ -175,6 +200,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.16 h1:rDLWLKQI4wAQVYp2GvJKqIyVGYTIl0myuP0lAbFfbfo= +github.com/tliron/commonlog v0.2.16/go.mod h1:p9p8LhdDgnOi9+GnF9tYKc8K61xOVr1reV+kpkSU7Cw= +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.5 h1:PNPFu54wn5uVPRt9GS/quRwdFZW4omSab9/dcFAsGmU= github.com/tmc/langchaingo v0.1.5/go.mod h1:RLtnUED/hH2v765vdjS9Z6gonErZAXURuJHph0BttqM= 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 99fb0ac4ee..4a5e912b88 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 +}