Skip to content

Commit

Permalink
feat: add LSP for FTL
Browse files Browse the repository at this point in the history
Add an LSP that produces document highlights when FTL outputs build errors

fixes #1122
  • Loading branch information
worstell committed Mar 26, 2024
1 parent 7579a4d commit 1411a2f
Show file tree
Hide file tree
Showing 26 changed files with 1,180 additions and 18 deletions.
3 changes: 2 additions & 1 deletion FTL.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"**/go.work.sum": true,
".hermit": true,
"**/*.zip": true,
}
},
"java.compile.nullAnalysis.mode": "automatic"
}
}
5 changes: 5 additions & 0 deletions cmd/ftl/cmd_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
"github.com/TBD54566975/ftl/lsp"
"time"

"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -43,6 +44,10 @@ func (d *devCmd) Run(ctx context.Context) error {
})
}

g.Go(func() error {
return lsp.NewServer(ctx).Run()
})

err := d.ServeCmd.pollControllerOnine(ctx, client)
if err != nil {
return err
Expand Down
22 changes: 22 additions & 0 deletions cmd/ftl/cmd_lsp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"context"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
)

type lspCmd struct {
Run runLspCmd `cmd:"" help:"Start the FTL language server."`
}

type runLspCmd struct {
Debug bool `help:"Run the language server in debug mode." default:"false"`
}

func (r *runLspCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceClient) error {
//server := lsp.NewServer(lsp.ServerOpts{
// Debug: r.Debug,
//})
//return server.Run()
return nil
}
1 change: 1 addition & 0 deletions cmd/ftl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type CLI struct {
Download downloadCmd `cmd:"" help:"Download a deployment."`
Secret secretCmd `cmd:"" help:"Manage secrets."`
Config configCmd `cmd:"" help:"Manage configuration."`
LSP lspCmd `cmd:"" help:"Run the FTL language server."`
}

var cli CLI
Expand Down
1 change: 0 additions & 1 deletion examples/go/echo/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package echo
import (
"context"
"fmt"

"ftl/time"

"github.com/TBD54566975/ftl/go-runtime/ftl"
Expand Down
12 changes: 3 additions & 9 deletions examples/kotlin/echo/src/main/kotlin/ftl/echo/Echo.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
package ftl.echo

import ftl.builtin.Empty
import ftl.time.time
import xyz.block.ftl.Context
import xyz.block.ftl.Export

class InvalidInput(val field: String) : Exception()
data class Request(val data: String)

data class EchoRequest(val name: String?)
data class EchoResponse(val message: String)

@Throws(InvalidInput::class)
@Export
fun echo(context: Context, req: EchoRequest): EchoResponse {
val response = context.call(::time, Empty())
return EchoResponse(message = "Hello, ${req.name ?: "anonymous"}! The time is ${response.time}.")
fun verb(context: Context, req: Request): Empty {
return Empty()
}
45 changes: 41 additions & 4 deletions go-runtime/compile/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,9 +542,16 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) (*schema.R
Pos: goPosToSchemaPos(pos),
Name: param.Obj().Name(),
})
typeArg, err := visitType(pctx, pos, named.TypeArgs().At(i))
if named.TypeArgs().Len() == 0 {

}
}

for i := 0; i < named.TypeArgs().Len(); i++ {
arg := named.TypeArgs().At(i)
typeArg, err := visitType(pctx, pos, arg)
if err != nil {
return nil, errorf(pos, "type parameter %s: %v", param.Obj().Name(), err)
return nil, errorf(pos, "type parameter %s: %v", arg.String(), err)
}
dataRef.TypeParameters = append(dataRef.TypeParameters, typeArg)
}
Expand All @@ -559,21 +566,32 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) (*schema.R
namedPos := named.Obj().Pos()
pkg, path, _ := pctx.pathEnclosingInterval(namedPos, namedPos)
if pkg != nil {
var doc *ast.CommentGroup
for i := len(path) - 1; i >= 0; i-- {
// We have to check both the type spec and the gen decl because the
// type could be declared as either "type Foo struct { ... }" or
// "type ( Foo struct { ... } )"
switch path := path[i].(type) {
case *ast.TypeSpec:
if path.Doc != nil {
out.Comments = visitComments(path.Doc)
doc = path.Doc
}
case *ast.GenDecl:
if path.Doc != nil {
out.Comments = visitComments(path.Doc)
doc = path.Doc
}
}
}
exported, err := isExported(doc)
if err != nil {
return nil, err
}

if doc == nil || !exported {
return nil, errorf(pos, "data type is a dependency of an exported verb or "+
"type but is not itself exported")
}
out.Comments = visitComments(doc)
}

s, ok := named.Underlying().(*types.Struct)
Expand Down Expand Up @@ -869,3 +887,22 @@ func isIotaEnum(node ast.Node) bool {
return false
}
}

func isExported(comments *ast.CommentGroup) (bool, error) {
if comments == nil {
return false, nil
}

directives, err := parseDirectives(fset, comments)
if err != nil {
return false, err
}

for _, d := range directives {
if _, ok := d.(*directiveExport); ok {
return true, nil
}
}

return false, nil
}
7 changes: 7 additions & 0 deletions go-runtime/compile/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,10 @@ func TestErrorReporting(t *testing.T) {
_, _, err := ExtractModuleSchema("testdata/failing")
assert.EqualError(t, err, filepath.Join(pwd, `testdata/failing/failing.go`)+`:14:2: call must have exactly three arguments`)
}

func TestUnexportedError(t *testing.T) {
pwd, _ := os.Getwd()
_, _, err := ExtractModuleSchema("testdata/unexported")
assert.EqualError(t, err, filepath.Join(pwd,
`testdata/unexported/unexported.go`)+`:13:1: data type is a dependency of an exported verb or type but is not itself exported`)
}
3 changes: 3 additions & 0 deletions go-runtime/compile/testdata/failing/failing.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"github.com/TBD54566975/ftl/go-runtime/ftl"
)

//ftl:export
type Request struct{}

//ftl:export
type Response struct{}

//ftl:export
Expand Down
7 changes: 7 additions & 0 deletions go-runtime/compile/testdata/one/one.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ const (
Third
)

//ftl:export
type Nested struct {
}

//ftl:export
type Req struct {
Int int
Int64 int64
Expand All @@ -66,8 +68,11 @@ type Req struct {
Bytes []byte
EnumRef two.TwoEnum
}

//ftl:export
type Resp struct{}

//ftl:export
type Config struct {
Field string
}
Expand All @@ -84,13 +89,15 @@ const Yellow Color = "Yellow"

const YellowInt ColorInt = 3

//ftl:export
type SinkReq struct{}

//ftl:export
func Sink(ctx context.Context, req SinkReq) error {
return nil
}

//ftl:export
type SourceResp struct{}

//ftl:export
Expand Down
3 changes: 3 additions & 0 deletions go-runtime/compile/testdata/two/two.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ const (
type Exported struct {
}

//ftl:export
type User struct {
Name string
}

//ftl:export
type Payload[T any] struct {
Body T
}

//ftl:export
type UserResponse struct {
User User
}
Expand Down
2 changes: 2 additions & 0 deletions go-runtime/compile/testdata/unexported/ftl.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module = "failing"
language = "go"
46 changes: 46 additions & 0 deletions go-runtime/compile/testdata/unexported/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module ftl/failing

go 1.22.1

replace github.com/TBD54566975/ftl => ../../../..

require github.com/TBD54566975/ftl v0.150.3

require (
connectrpc.com/connect v1.15.0 // indirect
connectrpc.com/grpcreflect v1.2.0 // indirect
connectrpc.com/otelconnect v0.7.0 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/TBD54566975/scaffolder v0.8.0 // indirect
github.com/alecthomas/concurrency v0.0.2 // indirect
github.com/alecthomas/kong v0.9.0 // indirect
github.com/alecthomas/participle/v2 v2.1.1 // indirect
github.com/alecthomas/types v0.13.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/swaggest/jsonschema-go v0.3.69 // indirect
github.com/swaggest/refl v1.3.0 // indirect
github.com/zalando/go-keyring v0.2.3 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
Loading

0 comments on commit 1411a2f

Please sign in to comment.