Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/three-buses-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/tsgo": minor
---

Add data-first and data-last piping flow normalization so data-first Effect and Layer APIs contribute the same flow structure as their pipeable forms.

This also extracts the shared bundled Effect test VFS helper into `internal/bundledeffect` and updates the affected flow and diagnostics baselines.
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ linters:
- path: internal/effecttest
linters:
- forbidigo
- path: internal/bundledeffect
linters:
- forbidigo
- path: _tools
linters:
- forbidigo
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ Each release of `effect-tsgo` is built against a specific upstream `tsgo` commit
"quickinfo": true,
// Controls Effect completions. (default: true)
"completions": true,
// Enables additional debug-only Effect language service output. (default: false)
"debug": false,
// Controls Effect goto references support. (default: true)
"goto": true,
// Controls Effect rename helpers. (default: true)
Expand Down
10 changes: 10 additions & 0 deletions etscore/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type EffectPluginOptions struct {
// Completions enables Effect completions in the language service.
Completions bool `json:"completions,omitzero" schema_description:"Controls Effect completions." schema_default:"true"`

// Debug enables extra debugging-oriented language service output.
Debug bool `json:"debug,omitzero" schema_description:"Enables additional debug-only Effect language service output." schema_default:"false"`

// Goto enables Effect goto/definition helpers in the language service.
Goto bool `json:"goto,omitzero" schema_description:"Controls Effect goto references support." schema_default:"true"`

Expand Down Expand Up @@ -218,6 +221,13 @@ func (e *EffectPluginOptions) GetCompletionsEnabled() bool {
return e.Completions
}

func (e *EffectPluginOptions) GetDebugEnabled() bool {
if e == nil {
return false
}
return e.Debug
}

// GetIncludeSuggestionsInTsc returns whether suggestion diagnostics should appear in tsc output.
// Returns true (include suggestions) when the receiver is nil.
func (e *EffectPluginOptions) GetIncludeSuggestionsInTsc() bool {
Expand Down
7 changes: 7 additions & 0 deletions etscore/options_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ func ParseFromPlugins(value any) *EffectPluginOptions {
}
}

// Parse debug (default: false)
if val, exists := getPluginValue("debug"); exists {
if b, ok := val.(bool); ok {
result.Debug = b
}
}

// Parse goto (default: true)
if val, exists := getPluginValue("goto"); exists {
if b, ok := val.(bool); ok {
Expand Down
17 changes: 17 additions & 0 deletions etscore/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ func TestParseFromPlugins_ExitCodeDefaults(t *testing.T) {
if opts.IgnoreEffectWarningsInTscExitCode {
t.Error("expected IgnoreEffectWarningsInTscExitCode to default to false")
}
if opts.GetDebugEnabled() {
t.Error("expected Debug to default to false")
}
}

func TestParseFromPlugins_DebugExplicitTrue(t *testing.T) {
plugins := makePlugins(makePluginMap(
"name", etscore.EffectPluginName,
"debug", true,
))
opts := etscore.ParseFromPlugins(plugins)
if opts == nil {
t.Fatal("expected non-nil options")
}
if !opts.GetDebugEnabled() {
t.Error("expected Debug to be true")
}
}

func TestParseFromPlugins_Overrides(t *testing.T) {
Expand Down
154 changes: 138 additions & 16 deletions etslshooks/document_symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package etslshooks
import (
"context"
"slices"
"strconv"
"strings"

"github.com/effect-ts/tsgo/internal/typeparser"
Expand All @@ -16,7 +17,8 @@ import (
)

func afterDocumentSymbols(ctx context.Context, sf *ast.SourceFile, symbols []*lsproto.DocumentSymbol, program *compiler.Program, langService *ls.LanguageService) []*lsproto.DocumentSymbol {
if program.Options().Effect == nil {
effectConfig := program.Options().Effect
if effectConfig == nil {
return symbols
}

Expand All @@ -28,11 +30,15 @@ func afterDocumentSymbols(ctx context.Context, sf *ast.SourceFile, symbols []*ls
serviceChildren := collectServiceDocumentSymbols(tp, c, sf, langService)
errorChildren := collectErrorDocumentSymbols(tp, c, sf, langService)
schemaChildren := collectSchemaDocumentSymbols(tp, c, sf, langService)
if len(layerChildren) == 0 && len(serviceChildren) == 0 && len(errorChildren) == 0 && len(schemaChildren) == 0 {
var flowChildren []*lsproto.DocumentSymbol
if effectConfig.GetDebugEnabled() {
flowChildren = collectFlowDocumentSymbols(tp, c, sf, langService)
}
if len(layerChildren) == 0 && len(serviceChildren) == 0 && len(errorChildren) == 0 && len(schemaChildren) == 0 && len(flowChildren) == 0 {
return symbols
}

effectChildren := make([]*lsproto.DocumentSymbol, 0, 4)
effectChildren := make([]*lsproto.DocumentSymbol, 0, 5)
if len(layerChildren) > 0 {
layers := newSyntheticNamespaceSymbol("Layers")
layers.Children = &layerChildren
Expand All @@ -53,6 +59,11 @@ func afterDocumentSymbols(ctx context.Context, sf *ast.SourceFile, symbols []*ls
schemas.Children = &schemaChildren
effectChildren = append(effectChildren, schemas)
}
if len(flowChildren) > 0 {
flows := newSyntheticNamespaceSymbol("Flows")
flows.Children = &flowChildren
effectChildren = append(effectChildren, flows)
}
effect := newSyntheticNamespaceSymbol("Effect")
effect.Children = &effectChildren

Expand Down Expand Up @@ -163,6 +174,58 @@ func collectSchemaDocumentSymbols(tp *typeparser.TypeParser, c *checker.Checker,
return symbols
}

func collectFlowDocumentSymbols(tp *typeparser.TypeParser, c *checker.Checker, sf *ast.SourceFile, langService *ls.LanguageService) []*lsproto.DocumentSymbol {
flows := tp.PipingFlows(sf, true)
if len(flows) == 0 {
return nil
}

symbols := make([]*lsproto.DocumentSymbol, 0, len(flows))
for i, flow := range flows {
if flow == nil || flow.Node == nil {
continue
}

children := make([]*lsproto.DocumentSymbol, 0, len(flow.Transformations)+1)
if flow.Subject.Node != nil {
children = append(children, newNamedDocumentSymbol(
sf,
langService,
flow.Subject.Node,
debugFlowNodeText(sf, flow.Subject.Node),
typeToDetail(c, flow.Subject.OutType, flow.Subject.Node),
layerSymbolKind(flow.Subject.Node),
))
}
for j, transformation := range flow.Transformations {
if transformation.Node == nil {
continue
}
children = append(children, newNamedDocumentSymbol(
sf,
langService,
transformation.Node,
strconv.Itoa(j)+": "+debugFlowTransformationText(sf, &transformation),
typeToDetail(c, transformation.OutType, transformation.Node),
lsproto.SymbolKindFunction,
))
}

flowSymbol := newNamedDocumentSymbol(
sf,
langService,
flow.Node,
"Flow "+strconv.Itoa(i),
nil,
lsproto.SymbolKindVariable,
)
flowSymbol.Children = &children
symbols = append(symbols, flowSymbol)
}

return symbols
}

func newSyntheticNamespaceSymbol(name string) *lsproto.DocumentSymbol {
children := []*lsproto.DocumentSymbol{}
zero := lsproto.Position{}
Expand All @@ -181,30 +244,37 @@ func newSyntheticNamespaceSymbol(name string) *lsproto.DocumentSymbol {
}
}

func newEffectDocumentSymbol(
tp *typeparser.TypeParser,
c *checker.Checker,
func newNamedDocumentSymbol(
sf *ast.SourceFile,
langService *ls.LanguageService,
node *ast.Node,
displayNode *ast.Node,
detail func(*typeparser.TypeParser, *checker.Checker, *ast.Node) *string,
name string,
detail *string,
kind lsproto.SymbolKind,
) *lsproto.DocumentSymbol {
children := []*lsproto.DocumentSymbol{}
if node == nil {
zero := lsproto.Position{}
return &lsproto.DocumentSymbol{
Name: name,
Detail: detail,
Kind: kind,
Range: lsproto.Range{Start: zero, End: zero},
SelectionRange: lsproto.Range{Start: zero, End: zero},
Children: &children,
}
}

converters := ls.LanguageService_converters(langService)
startPos := scanner.SkipTrivia(sf.Text(), node.Pos())
endPos := max(startPos, node.End())
start := converters.PositionToLineAndCharacter(sf, core.TextPos(startPos))
end := converters.PositionToLineAndCharacter(sf, core.TextPos(endPos))
children := []*lsproto.DocumentSymbol{}
var symbolDetail *string
if detail != nil {
symbolDetail = detail(tp, c, node)
}

return &lsproto.DocumentSymbol{
Name: layerSymbolName(sf, displayNode),
Detail: symbolDetail,
Kind: layerSymbolKind(displayNode),
Name: name,
Detail: detail,
Kind: kind,
Range: lsproto.Range{
Start: start,
End: end,
Expand All @@ -217,6 +287,58 @@ func newEffectDocumentSymbol(
}
}

func newEffectDocumentSymbol(
tp *typeparser.TypeParser,
c *checker.Checker,
sf *ast.SourceFile,
langService *ls.LanguageService,
node *ast.Node,
displayNode *ast.Node,
detail func(*typeparser.TypeParser, *checker.Checker, *ast.Node) *string,
) *lsproto.DocumentSymbol {
children := []*lsproto.DocumentSymbol{}
var symbolDetail *string
if detail != nil {
symbolDetail = detail(tp, c, node)
}

symbol := newNamedDocumentSymbol(sf, langService, node, layerSymbolName(sf, displayNode), symbolDetail, layerSymbolKind(displayNode))
symbol.Children = &children
return symbol
}

func typeToDetail(c *checker.Checker, t *checker.Type, node *ast.Node) *string {
if c == nil || t == nil || node == nil {
return nil
}
detail := c.TypeToStringEx(t, node, checker.TypeFormatFlagsNoTruncation)
return &detail
}

func debugFlowNodeText(sf *ast.SourceFile, node *ast.Node) string {
if node == nil {
return "<unknown>"
}
text := strings.Join(strings.Fields(scanner.GetSourceTextOfNodeFromSourceFile(sf, node, false)), " ")
if text == "" {
return "<unknown>"
}
if len(text) > 80 {
return text[:77] + "..."
}
return text
}

func debugFlowTransformationText(sf *ast.SourceFile, transformation *typeparser.PipingFlowTransformation) string {
if transformation == nil {
return "<unknown>"
}
if transformation.Callee != nil {
return debugFlowNodeText(sf, transformation.Callee)
}
return debugFlowNodeText(sf, transformation.Node)
}

func layerSymbolDetail(tp *typeparser.TypeParser, c *checker.Checker, node *ast.Node) *string {
typeCheckNode, types := classificationTypes(tp, c, node)
for _, t := range types {
Expand Down
20 changes: 10 additions & 10 deletions etstesthooks/doc.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Package etstesthooks provides automatic Effect package mounting for fourslash tests.
//
// This package registers a PrepareTestFSCallback that detects Effect imports in
// fourslash test files and automatically mounts the real Effect node_modules into
// the test VFS via effecttest.MountEffect.
//
// Import this package with a blank import in test files that use fourslash with Effect:
//
// import _ "github.com/effect-ts/tsgo/etstesthooks"
package etstesthooks
// Package etstesthooks provides automatic Effect package mounting for fourslash tests.
//
// This package registers a PrepareTestFSCallback that detects Effect imports in
// fourslash test files and automatically mounts the real Effect node_modules into
// the test VFS via vfstest.MountEffect.
//
// Import this package with a blank import in test files that use fourslash with Effect:
//
// import _ "github.com/effect-ts/tsgo/etstesthooks"
package etstesthooks
Loading
Loading