diff --git a/backend/go/BUILD b/backend/go/BUILD index f0729766..90f785b1 100644 --- a/backend/go/BUILD +++ b/backend/go/BUILD @@ -245,7 +245,7 @@ def go_mod( out = "/**/BUILD", deps = {"cfg": godeps_cfg}, runtime_deps = {"deps": godeps_deps}, - hash_deps = [imports, gomodsum], + hash_deps = [gomodsum, imports] + mod_pkg_modsums, tools = godeps, env = { "GOOS": get_os(), diff --git a/cmd/heph/lsp.go b/cmd/heph/lsp.go new file mode 100644 index 00000000..dbcac356 --- /dev/null +++ b/cmd/heph/lsp.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + + "github.com/hephbuild/heph/bootstrap" + "github.com/hephbuild/heph/lsp" + "github.com/spf13/cobra" +) + +func init() { + lspCommand.AddCommand(servelspCmd) +} + +var lspCommand = &cobra.Command{ + Use: "lsp", + Aliases: []string{"lsp"}, + Short: "Heph Language Server", + Args: cobra.RangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("run heph Language Server with `serve`") + }, +} + +var servelspCmd = &cobra.Command{ + Use: "serve", + Short: "Serve LSP", + Aliases: []string{"s"}, + ValidArgsFunction: ValidArgsFunctionTargets, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + localOpt := bootstrap.BootOpts{} + bs, err := bootstrap.BootBase(ctx, localOpt) + if err != nil { + return err + } + + server, err := lsp.NewHephServer(bs.Root) + if err != nil { + return err + } + + return server.Serve() + }, +} diff --git a/cmd/heph/root.go b/cmd/heph/root.go index a6804e2d..20bf8e41 100644 --- a/cmd/heph/root.go +++ b/cmd/heph/root.go @@ -86,6 +86,7 @@ func init() { rootCmd.AddCommand(runCmd) rootCmd.AddCommand(cleanCmd) rootCmd.AddCommand(queryCmd) + rootCmd.AddCommand(lspCommand) rootCmd.AddCommand(cloudCmd) rootCmd.AddCommand(gcCmd) rootCmd.AddCommand(validateCmd) diff --git a/cmd/heph/utils.go b/cmd/heph/utils.go index 3603ea53..6f40e327 100644 --- a/cmd/heph/utils.go +++ b/cmd/heph/utils.go @@ -11,10 +11,7 @@ func ValidArgsFunctionTargets(cmd *cobra.Command, args []string, toComplete stri } directive := cobra.ShellCompDirectiveNoFileComp - isFuzzy, suggestions := autocompleteTargetName(targets, toComplete) - if isFuzzy { - directive |= cobra.ShellCompDirectiveNoMatching - } + _, suggestions := autocompleteTargetName(targets, toComplete) return suggestions, directive } @@ -26,10 +23,7 @@ func ValidArgsFunctionLabelsOrTargets(cmd *cobra.Command, args []string, toCompl } directive := cobra.ShellCompDirectiveNoFileComp - isFuzzy, suggestions := autocompleteLabelOrTarget(targets, labels, toComplete) - if isFuzzy { - directive |= cobra.ShellCompDirectiveNoMatching - } + _, suggestions := autocompleteLabelOrTarget(targets, labels, toComplete) return suggestions, directive } diff --git a/go.mod b/go.mod index 13fcd2bf..416c047e 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ module github.com/hephbuild/heph -go 1.22 +go 1.25.1 replace github.com/spf13/cobra v1.7.0 => github.com/raphaelvigee/cobra v0.0.0-20221020122344-217ca52feee0 require ( - cloud.google.com/go/storage v1.34.1 github.com/Khan/genqlient v0.6.0 github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec @@ -18,10 +17,10 @@ require ( github.com/charmbracelet/bubbletea v0.26.2 github.com/charmbracelet/lipgloss v0.9.1 github.com/coreos/go-semver v0.3.1 - github.com/creack/pty v1.1.20 + github.com/creack/pty v1.1.24 github.com/dlsniper/debugger v0.6.0 - github.com/fsnotify/fsnotify v1.7.0 - github.com/google/uuid v1.4.0 + github.com/fsnotify/fsnotify v1.9.0 + github.com/google/uuid v1.6.0 github.com/heimdalr/dag v1.3.1 github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 github.com/itchyny/gojq v0.12.13 @@ -29,13 +28,17 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.15.2 + github.com/muesli/termenv v0.16.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/shirou/gopsutil/v3 v3.23.10 - github.com/spf13/cobra v1.7.0 - github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.10.0 + github.com/tliron/commonlog v0.2.21 + github.com/tliron/glsp v0.2.2 + github.com/tree-sitter/go-tree-sitter v0.25.0 + github.com/tree-sitter/tree-sitter-python v0.25.0 github.com/viney-shih/go-lock v1.1.2 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/zeebo/xxh3 v1.0.2 @@ -46,18 +49,17 @@ require ( go.starlark.net v0.0.0-20231101134539-556fd59b42f6 go.uber.org/multierr v1.11.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.17.0 - golang.org/x/sys v0.20.0 - golang.org/x/term v0.20.0 - google.golang.org/api v0.149.0 + golang.org/x/net v0.43.0 + golang.org/x/sys v0.36.0 + golang.org/x/term v0.35.0 gopkg.in/yaml.v3 v3.0.1 ) require ( cloud.google.com/go v0.110.10 // indirect - cloud.google.com/go/compute v1.23.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/storage v1.34.1 // indirect github.com/RoaringBitmap/roaring v1.6.0 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/alexflint/go-arg v1.4.3 // indirect @@ -80,10 +82,10 @@ require ( github.com/blevesearch/zapx/v13 v13.3.10 // indirect github.com/blevesearch/zapx/v14 v14.3.10 // indirect github.com/blevesearch/zapx/v15 v15.3.13 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect @@ -93,6 +95,8 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -103,41 +107,47 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-pointer v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.6 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect - github.com/rivo/uniseg v0.4.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/sasha-s/go-deadlock v0.3.6 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/stretchr/objx v0.5.1 // indirect + github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tliron/go-kutil v0.4.0 // indirect github.com/vektah/gqlparser/v2 v2.5.10 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.etcd.io/bbolt v1.3.8 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.14.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/appengine v1.6.8 // indirect + google.golang.org/api v0.149.0 // indirect google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d3516665..6c848baf 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g= @@ -91,13 +89,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4= -github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dlsniper/debugger v0.6.0 h1:AyPoOtJviCmig9AKNRAPPw5B5UyB+cI72zY3Jb+6LlA= @@ -112,13 +111,13 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsouza/fake-gcs-server v1.47.4 h1:gfBhBxEra20/Om02cvcyL8EnekV8KDb01Yffjat6AKQ= github.com/fsouza/fake-gcs-server v1.47.4/go.mod h1:vqUZbI12uy9IkRQ54Q4p5AniQsSiUq8alO9Nv2egMmA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -143,7 +142,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -157,8 +155,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= @@ -168,8 +167,8 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -178,11 +177,15 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/heimdalr/dag v1.3.1 h1:EVFVwlQQF3BkG5KptfhY645enDUakmpOe9GmOYYtKB8= github.com/heimdalr/dag v1.3.1/go.mod h1:OCh6ghKmU0hPjtwMqWBoNxPmtRioKd1xSu7Zs4sbIqM= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= @@ -219,6 +222,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -228,8 +233,9 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -238,12 +244,14 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -251,23 +259,26 @@ github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/raphaelvigee/cobra v0.0.0-20221020122344-217ca52feee0 h1:M4TK6Jyh2wDKpJcK0JLEYxC+EFiGLk/OQsSFi7cALUw= -github.com/raphaelvigee/cobra v0.0.0-20221020122344-217ca52feee0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= -github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= +github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= +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/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM= @@ -278,13 +289,18 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -292,13 +308,45 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tliron/commonlog v0.2.21 h1:V1v+6opmzuOqDxxnxxM5RWtlHZmqZlDxkKeZGs6DpPg= +github.com/tliron/commonlog v0.2.21/go.mod h1:W6XVoS/zo7mHXv2Kz8HKnBq+U34dFysJ2KUh2Aboibw= +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/go-kutil v0.4.0 h1:5JwcBacgnqS3XyhwCWZKvq8ftlbVttNXnt+kfCH+Y2E= +github.com/tliron/go-kutil v0.4.0/go.mod h1:hpHVq+CP1uci2M208UEjPiPwsRsz/QweGBnLB3CaQ24= +github.com/tree-sitter/go-tree-sitter v0.25.0 h1:sx6kcg8raRFCvc9BnXglke6axya12krCJF5xJ2sftRU= +github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU= +github.com/tree-sitter/tree-sitter-c v0.23.4 h1:nBPH3FV07DzAD7p0GfNvXM+Y7pNIoPenQWBpvM++t4c= +github.com/tree-sitter/tree-sitter-c v0.23.4/go.mod h1:MkI5dOiIpeN94LNjeCp8ljXN/953JCwAby4bClMr6bw= +github.com/tree-sitter/tree-sitter-cpp v0.23.4 h1:LaWZsiqQKvR65yHgKmnaqA+uz6tlDJTJFCyFIeZU/8w= +github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8= +github.com/tree-sitter/tree-sitter-embedded-template v0.23.2 h1:nFkkH6Sbe56EXLmZBqHHcamTpmz3TId97I16EnGy4rg= +github.com/tree-sitter/tree-sitter-embedded-template v0.23.2/go.mod h1:HNPOhN0qF3hWluYLdxWs5WbzP/iE4aaRVPMsdxuzIaQ= +github.com/tree-sitter/tree-sitter-go v0.23.4 h1:yt5KMGnTHS+86pJmLIAZMWxukr8W7Ae1STPvQUuNROA= +github.com/tree-sitter/tree-sitter-go v0.23.4/go.mod h1:Jrx8QqYN0v7npv1fJRH1AznddllYiCMUChtVjxPK040= +github.com/tree-sitter/tree-sitter-html v0.23.2 h1:1UYDV+Yd05GGRhVnTcbP58GkKLSHHZwVaN+lBZV11Lc= +github.com/tree-sitter/tree-sitter-html v0.23.2/go.mod h1:gpUv/dG3Xl/eebqgeYeFMt+JLOY9cgFinb/Nw08a9og= +github.com/tree-sitter/tree-sitter-java v0.23.5 h1:J9YeMGMwXYlKSP3K4Us8CitC6hjtMjqpeOf2GGo6tig= +github.com/tree-sitter/tree-sitter-java v0.23.5/go.mod h1:NRKlI8+EznxA7t1Yt3xtraPk1Wzqh3GAIC46wxvc320= +github.com/tree-sitter/tree-sitter-javascript v0.23.1 h1:1fWupaRC0ArlHJ/QJzsfQ3Ibyopw7ZfQK4xXc40Zveo= +github.com/tree-sitter/tree-sitter-javascript v0.23.1/go.mod h1:lmGD1EJdCA+v0S1u2fFgepMg/opzSg/4pgFym2FPGAs= +github.com/tree-sitter/tree-sitter-json v0.24.8 h1:tV5rMkihgtiOe14a9LHfDY5kzTl5GNUYe6carZBn0fQ= +github.com/tree-sitter/tree-sitter-json v0.24.8/go.mod h1:F351KK0KGvCaYbZ5zxwx/gWWvZhIDl0eMtn+1r+gQbo= +github.com/tree-sitter/tree-sitter-php v0.23.11 h1:iHewsLNDmznh8kgGyfWfujsZxIz1YGbSd2ZTEM0ZiP8= +github.com/tree-sitter/tree-sitter-php v0.23.11/go.mod h1:T/kbfi+UcCywQfUNAJnGTN/fMSUjnwPXA8k4yoIks74= +github.com/tree-sitter/tree-sitter-python v0.25.0 h1:O6XD9v8U1LOcRc3cNj9nM7XufrtEBezE6VrpRrHZDf0= +github.com/tree-sitter/tree-sitter-python v0.25.0/go.mod h1:cpdthSy/Yoa28aJFBscFHlGiU+cnSiSh1kuDVtI8YeM= +github.com/tree-sitter/tree-sitter-ruby v0.23.1 h1:T/NKHUA+iVbHM440hFx+lzVOzS4dV6z8Qw8ai+72bYo= +github.com/tree-sitter/tree-sitter-ruby v0.23.1/go.mod h1:kUS4kCCQloFcdX6sdpr8p6r2rogbM6ZjTox5ZOQy8cA= +github.com/tree-sitter/tree-sitter-rust v0.23.2 h1:6AtoooCW5GqNrRpfnvl0iUhxTAZEovEmLKDbyHlfw90= +github.com/tree-sitter/tree-sitter-rust v0.23.2/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI= github.com/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/viney-shih/go-lock v1.1.2 h1:3TdGTiHZCPqBdTvFbQZQN/TRZzKF3KWw2rFEyKz3YqA= @@ -336,8 +384,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= @@ -346,8 +394,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -359,19 +407,19 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -389,23 +437,22 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -414,8 +461,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= @@ -424,8 +471,6 @@ google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= @@ -453,8 +498,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/lsp/capabilities/diagnostic/diagnostic.go b/lsp/capabilities/diagnostic/diagnostic.go new file mode 100644 index 00000000..781067ad --- /dev/null +++ b/lsp/capabilities/diagnostic/diagnostic.go @@ -0,0 +1,5 @@ +package diagnostic + + +// TODO: Implement diagnostics +// For this we need glsp protocol_3_17 diff --git a/lsp/capabilities/lang/common.go b/lsp/capabilities/lang/common.go new file mode 100644 index 00000000..22f4a3e8 --- /dev/null +++ b/lsp/capabilities/lang/common.go @@ -0,0 +1,10 @@ +package lang + +import "strings" + +func addProtocol(uri string) string { + if !strings.HasPrefix(uri, "file://") { + uri = "file://" + uri + } + return uri +} diff --git a/lsp/capabilities/lang/completion.go b/lsp/capabilities/lang/completion.go new file mode 100644 index 00000000..af25fdeb --- /dev/null +++ b/lsp/capabilities/lang/completion.go @@ -0,0 +1,93 @@ +package lang + +import ( + "github.com/hephbuild/heph/lsp/runtime" + "github.com/hephbuild/heph/lsp/runtime/document" + "github.com/hephbuild/heph/lsp/runtime/symbol" + "github.com/tliron/commonlog" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +var Logger = commonlog.GetLogger("completion") + +func TextDocumentCompletionFuncWrapper(manager *runtime.Manager) protocol.TextDocumentCompletionFunc { + return func(context *glsp.Context, params *protocol.CompletionParams) (any, error) { + var completionItems []protocol.CompletionItem + + if doc, found := manager.GetDocument(params.TextDocument.URI); found { + byteOffset := params.Position.IndexIn(doc.TextString) + + // If is function we can get args completion + funName := doc.ExtractCurrentFunctionName(uint(byteOffset)) + if s, found := manager.Query(funName); found { + if s.Is(symbol.FunctionKind) { + args := s.Parameters + if args != nil { + completionItems = createCompletionItemForArgs(args, s) + } + } + } + + // Append current doc symbols + for _, s := range doc.Symbols { + compItem := createCompletionItem(s) + completionItems = append(completionItems, compItem) + } + + // Complete symbols that are loaded by "load" + doc.RangeDocLoads(func(load *document.Load) { + loadDoc := load.Doc + if syms := loadDoc.QueryAll(load.Loads); len(syms) > 0 { + for _, sym := range syms { + compItem := createCompletionItem(sym) + completionItems = append(completionItems, compItem) + } + } + }) + + } + + // Load Builtin + allPerKind := manager.BuiltinSymbols + for _, symbol := range allPerKind { + compItem := createCompletionItem(symbol) + completionItems = append(completionItems, compItem) + } + + return completionItems, nil + } +} + +func createCompletionItem(symbol *symbol.Symbol) protocol.CompletionItem { + name := symbol.Name + sig := symbol.Signature + doc := symbol.DocString + kind := SymbolKindToCompletionKind(symbol.Kind) + + compItem := protocol.CompletionItem{ + Label: name, + InsertText: &name, + Kind: &kind, + Detail: &sig, + Documentation: doc, + } + return compItem +} + +func createCompletionItemForArgs(args []*symbol.Parameter, s *symbol.Symbol) []protocol.CompletionItem { + completionItems := []protocol.CompletionItem{} + for _, arg := range args { + kind := protocol.CompletionItemKindVariable + + label := arg.Name + "=" + completionItems = append(completionItems, protocol.CompletionItem{ + Label: label, + InsertText: &label, + Kind: &kind, + Documentation: s.DocString, + }) + } + + return completionItems +} diff --git a/lsp/capabilities/lang/converter.go b/lsp/capabilities/lang/converter.go new file mode 100644 index 00000000..555fe7f1 --- /dev/null +++ b/lsp/capabilities/lang/converter.go @@ -0,0 +1,22 @@ +package lang + +import ( + "github.com/hephbuild/heph/lsp/runtime/symbol" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func SymbolKindToCompletionKind(kind symbol.SymbolKind) protocol.CompletionItemKind { + switch kind { + case symbol.FunctionKind: + return protocol.CompletionItemKindFunction + + case symbol.VariableKind: + return protocol.CompletionItemKindVariable + + case symbol.ClassKind: + return protocol.CompletionItemKindClass + + default: + return protocol.CompletionItemKindValue + } +} diff --git a/lsp/capabilities/lang/declaration.go b/lsp/capabilities/lang/declaration.go new file mode 100644 index 00000000..77f92ad5 --- /dev/null +++ b/lsp/capabilities/lang/declaration.go @@ -0,0 +1,137 @@ +package lang + +import ( + "path" + "strings" + + "github.com/hephbuild/heph/lsp/runtime" + "github.com/hephbuild/heph/lsp/runtime/document" + "github.com/hephbuild/heph/lsp/runtime/symbol" + "github.com/tliron/commonlog" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +var logger = commonlog.GetLogger("lifecycle") + +func TextDocumentDeclarationFuncWrapper(manager *runtime.Manager) protocol.TextDocumentDeclarationFunc { + return func(context *glsp.Context, params *protocol.DeclarationParams) (any, error) { + if location, found := extractLocation(manager, params.TextDocument.URI, ¶ms.Position); found { + logger.Noticef("Declaration location: %v", location) + return location, nil + } + + return nil, nil + } +} + +func TextDocumentDefinitionFuncWrapper(manager *runtime.Manager) protocol.TextDocumentDefinitionFunc { + return func(context *glsp.Context, params *protocol.DefinitionParams) (any, error) { + if location, found := extractLocation(manager, params.TextDocument.URI, ¶ms.Position); found { + logger.Noticef("Declaration location: %v", location) + return location, nil + } + + return nil, nil + } +} + +func extractLocation(manager *runtime.Manager, uri string, pos *protocol.Position) (*protocol.Location, bool) { + if doc, found := manager.GetDocument(uri); found { + pos := uint(pos.IndexIn(doc.TextString)) + + // If its target address, open from current workspace + if literal := doc.ExtractCurrentStringLiteral(pos); literal != "" { + // Check if its a target location + if strings.HasPrefix(literal, "//") { + + // Fallback to default BUILD file of that directory since we don't know where that heph can be + literal = strings.Split(literal, ":")[0] + literal = path.Join(literal, "BUILD") + fullPath := path.Join(manager.WorkspaceFolder, literal) + + return &protocol.Location{ + URI: addProtocol(fullPath), + Range: protocol.Range{ + Start: protocol.Position{ + Line: 0, + Character: 0, + }, + End: protocol.Position{ + Line: 0, + Character: 0, + }, + }, + }, true + } + } + + if symbolName := doc.ExtractCurrentSymbolName(pos); symbolName != "" { + // Current doc + if sym, found := doc.Query(symbolName); found { + return buildLocationFromSymbol(doc.FullPath, sym), true + } + + // Loaded docs + var loc *protocol.Location + var symbolFound bool + + // Location symbols that are loaded by "load" + doc.RangeDocLoads(func(load *document.Load) { + if !symbolFound { + doc := load.Doc + if sym, found := doc.Query(symbolName); found { + loc = buildLocationFromSymbol(doc.FullPath, sym) + symbolFound = true + } + } + }) + + if symbolFound { + return loc, true + } + } + + if symbolName := doc.ExtractCurrentFunctionName(pos); symbolName != "" { + // Current doc + if sym, found := doc.Query(symbolName); found { + return buildLocationFromSymbol(doc.FullPath, sym), true + } + + // Loaded docs + var loc *protocol.Location + var symbolFound bool + doc.RangeDocLoads(func(load *document.Load) { + if !symbolFound { + doc := load.Doc + if sym, found := doc.Query(symbolName); found { + loc = buildLocationFromSymbol(doc.FullPath, sym) + symbolFound = true + } + } + }) + + if symbolFound { + return loc, true + } + } + } + + return nil, false +} + +func buildLocationFromSymbol(uri string, sym *symbol.Symbol) *protocol.Location { + return &protocol.Location{ + URI: addProtocol(uri), + Range: protocol.Range{ + Start: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowStart), + Character: protocol.UInteger(sym.Position.ColumnStart), + }, + End: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowEnd), + Character: protocol.UInteger(sym.Position.ColumnEnd), + }, + }, + } +} diff --git a/lsp/capabilities/lang/hover.go b/lsp/capabilities/lang/hover.go new file mode 100644 index 00000000..26fab697 --- /dev/null +++ b/lsp/capabilities/lang/hover.go @@ -0,0 +1,135 @@ +package lang + +import ( + "fmt" + "strings" + + "github.com/hephbuild/heph/lsp/runtime" + "github.com/hephbuild/heph/lsp/runtime/symbol" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TextDocumentHoverFuncWrapper(manager *runtime.Manager) protocol.TextDocumentHoverFunc { + return func(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) { + doc, ok := manager.GetDocument(params.TextDocument.URI) + if !ok { + return &protocol.Hover{}, nil + } + + bytePos := params.Position.IndexIn(doc.TextString) + + if literal := doc.ExtractCurrentStringLiteral(uint(bytePos)); literal != "" { + return createLiteralHover(literal), nil + } + + symbolName := doc.ExtractCurrentSymbolName(uint(bytePos)) + + // If is an argument inside a function call we can get the function name and args information + funName := doc.ExtractCurrentFunctionName(uint(bytePos)) + if s, found := manager.Query(funName); found { + for _, p := range s.Parameters { + if strings.Contains(p.Name, symbolName) { + return createArgHover(s, p), nil + } + } + } + + // Query first for current document symbols + if symbol, found := doc.Query(symbolName); found { + return createHover(symbol), nil + } + + if symbol, found := manager.Query(symbolName); found { + return createHover(symbol), nil + } + + return nil, nil + } +} + +func createHover(sym *symbol.Symbol) *protocol.Hover { + var sb strings.Builder + + sb.WriteString(sym.Source) + sb.WriteString("\n---\n") + + if sym.Is(symbol.VariableKind) { + varSignature := langDecorateMultiline(sym.Name+" = "+sym.Value, runtime.HephLanguage) + sb.WriteString(varSignature) + } else { + sb.WriteString(langDecorateMultiline(sym.Signature, runtime.HephLanguage)) + } + + sb.WriteString("\n---\n") + sb.WriteString(sym.DocString) + + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: sb.String(), + }, + Range: &protocol.Range{ + Start: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowStart), + Character: protocol.UInteger(sym.Position.ColumnStart), + }, + End: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowEnd), + Character: protocol.UInteger(sym.Position.ColumnEnd), + }, + }, + } +} + +func createArgHover(fn *symbol.Symbol, param *symbol.Parameter) *protocol.Hover { + var sb strings.Builder + + sb.WriteString(param.Name) + + if param.Type != "" { + sb.WriteString(":" + param.Type) + } + + if param.Value != "" { + sb.WriteString(" = " + param.Value) + } + + sb.WriteString(" from " + langDecorate(fn.Name)) + if param.DocString != "" { + sb.WriteString("\n---\n") + sb.WriteString(param.DocString) + } + + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: sb.String(), + }, + } +} + +func createLiteralHover(literal string) *protocol.Hover { + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: parseDefinition(literal), + }, + } +} + +func langDecorateMultiline(text, lang string) string { + return "```" + lang + "\n" + text + "\n```\n" +} + +func langDecorate(text string) string { + return "`" + text + "`" +} + +func parseDefinition(literal string) string { + if ok := strings.HasPrefix(literal, "//"); ok { + return fmt.Sprintf("Heph Package: %q", literal) + } + + return literal +} diff --git a/lsp/capabilities/lang/references.go b/lsp/capabilities/lang/references.go new file mode 100644 index 00000000..f3f28a74 --- /dev/null +++ b/lsp/capabilities/lang/references.go @@ -0,0 +1,79 @@ +package lang + +import ( + "github.com/hephbuild/heph/lsp/runtime" + "github.com/hephbuild/heph/lsp/runtime/document" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TextDocumentReferencesFuncWrapper(manager *runtime.Manager) protocol.TextDocumentReferencesFunc { + return func(context *glsp.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) { + if doc, found := manager.GetDocument(params.TextDocument.URI); found { + pos := uint(params.Position.IndexIn(doc.TextString)) + + if symbolName := doc.ExtractCurrentSymbolName(pos); symbolName != "" { + return findReferences(doc, symbolName), nil + } + + if symbolName := doc.ExtractCurrentFunctionName(pos); symbolName != "" { + return findReferences(doc, symbolName), nil + } + } + + return nil, nil + } +} + +// TODO: We will probably later need a better usage of symbols, +// having a symbol oriented query instead of doc based queries +func findReferences(doc *document.Document, symbolName string) []protocol.Location { + var locations []protocol.Location + + // Find references in the doc itself + if calls := doc.QueryCalls(symbolName); len(calls) > 0 { + for _, sym := range calls { + loc := protocol.Location{ + URI: addProtocol(doc.FullPath), + Range: protocol.Range{ + Start: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowStart), + Character: protocol.UInteger(sym.Position.ColumnStart), + }, + End: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowEnd), + Character: protocol.UInteger(sym.Position.ColumnEnd), + }, + }, + } + + locations = append(locations, loc) + } + } + + // Search for docs that loads current doc and method + doc.RangeIsLoadedBy(func(load *document.Load) { + otherDoc := load.Doc + if calls := otherDoc.QueryCalls(symbolName); len(calls) > 0 { + for _, sym := range calls { + loc := protocol.Location{ + URI: addProtocol(otherDoc.FullPath), + Range: protocol.Range{ + Start: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowStart), + Character: protocol.UInteger(sym.Position.ColumnStart), + }, + End: protocol.Position{ + Line: protocol.UInteger(sym.Position.RowEnd), + Character: protocol.UInteger(sym.Position.ColumnEnd), + }, + }, + } + + locations = append(locations, loc) + } + } + }) + + return locations +} diff --git a/lsp/capabilities/lifecycle/initialize.go b/lsp/capabilities/lifecycle/initialize.go new file mode 100644 index 00000000..601eda8f --- /dev/null +++ b/lsp/capabilities/lifecycle/initialize.go @@ -0,0 +1,45 @@ +package lifecycle + +import ( + "os" + + "github.com/hephbuild/heph/lsp/runtime" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func InitializeCallback(manager *runtime.Manager, context *glsp.Context, params *protocol.InitializeParams) error { + root := "" + + if params.RootPath != nil { + root = *params.RootPath + } + + if params.RootURI != nil { + root = *params.RootURI + } + + if root != "" { + manager.WorkspaceFolder = root + + return nil + } + + wkFolders := params.WorkspaceFolders + if len(wkFolders) >= 1 { + newVar := wkFolders[0] + manager.WorkspaceFolder = newVar.URI + + return nil + } + + v, err := os.Getwd() + if err != nil { + return err + } + + // Fallback to cwd + manager.WorkspaceFolder = v + + return nil +} diff --git a/lsp/capabilities/sync/sync.go b/lsp/capabilities/sync/sync.go new file mode 100644 index 00000000..ff373997 --- /dev/null +++ b/lsp/capabilities/sync/sync.go @@ -0,0 +1,167 @@ +package sync + +import ( + "errors" + + "github.com/hephbuild/heph/lsp/runtime" + "github.com/tliron/glsp" + tree_sitter "github.com/tree-sitter/go-tree-sitter" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization + +var ( + ErrInvalidTree = errors.New("invalid tree") + ErrInvalidDoc = errors.New("invalid doc") +) + +func TextDocumentDidOpenWrapper(manager *runtime.Manager) protocol.TextDocumentDidOpenFunc { + + return func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error { + parser := manager.Parser + text := params.TextDocument.Text + bts := []byte(text) + + if doc, ok := manager.GetDocument(params.TextDocument.URI); ok { + newTree := parser.Parse(bts, nil) + if newTree == nil { + return ErrInvalidTree + } + + _, err := manager.SwapTree(doc, newTree, bts) + + return errors.Join(ErrInvalidTree, err) + } + + newTree := parser.Parse(bts, nil) + if newTree == nil { + return ErrInvalidTree + } + + version := params.TextDocument.Version + _, err := manager.NewDocument(params.TextDocument.URI, newTree, bts, version) + if err != nil { + newTree.Close() + return ErrInvalidTree + } + + + return nil + } +} + +func TextDocumentDidChangeFuncWrapper(manager *runtime.Manager) protocol.TextDocumentDidChangeFunc { + return func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error { + parser := manager.Parser + + doc, ok := manager.GetDocument(params.TextDocument.URI) + if !ok { + return ErrInvalidDoc + } + + for _, change := range params.ContentChanges { + if event, ok := change.(protocol.TextDocumentContentChangeEvent); ok { + text := doc.TextString + insertBytes := []byte(event.Text) + + startByteOffset, endByteOffset := event.Range.IndexesIn(text) + endByte := uint(endByteOffset) + uint(len(insertBytes)) + + editInput := tree_sitter.InputEdit{ + StartByte: uint(startByteOffset), + OldEndByte: uint(endByteOffset), + NewEndByte: endByte, + + StartPosition: tree_sitter.Point{ + Row: uint(event.Range.Start.Line), + Column: uint(event.Range.Start.Character), + }, + OldEndPosition: tree_sitter.Point{ + Row: uint(event.Range.End.Line), + Column: uint(event.Range.End.Character), + }, + NewEndPosition: tree_sitter.Point{ + Row: uint(event.Range.End.Line), + Column: endByte * 2, + }, + } + + doc.Tree.Edit(&editInput) + newText := ParseNewBytes(doc.Text, insertBytes, startByteOffset, endByteOffset) + newTree := parser.Parse(newText, doc.Tree) + + _, err := manager.SwapTree(doc, newTree, newText) + if err != nil { + return errors.Join(ErrInvalidTree, err) + } + } + + if event, ok := change.(protocol.TextDocumentContentChangeEventWhole); ok { + bts := []byte(event.Text) + newTree := parser.Parse(bts, nil) + + _, err := manager.SwapTree(doc, newTree, bts) + if err != nil { + newTree.Close() + return errors.Join(ErrInvalidTree, err) + } + } + } + + return nil + } +} + +// ParseNewBytes creates a new byte array to insert changes from client +// Later we can check if we can do it inplace +func ParseNewBytes(current, insert []byte, offsetStart, offsetEnd int) []byte { + diff := offsetEnd - offsetStart + newLen := len(current) + len(insert) - diff + newSlice := make([]byte, newLen) + + // Corner case: empty insert "" means delete + if len(insert) == 0 { + insertIndex := 0 + for i := 0; i < len(current); i++ { + if i >= offsetStart && i < offsetEnd { + continue + } + + newSlice[insertIndex] = current[i] + insertIndex++ + } + + return newSlice + } + + // In the same request we can have Replace or Insert + // Replace: offsetEnd > i, meaning we will overwrite any char until that offset + // Insert: Always insert + insertIndex := 0 + currentIndex := 0 + for i := 0; i < newLen; i++ { + if i >= offsetStart { + // Replace + if i < offsetEnd { + currentIndex++ + + } + + // Insert + if insertIndex < len(insert) { + newSlice[i] = insert[insertIndex] + insertIndex++ + + continue + } + } + + // Copy + newSlice[i] = current[currentIndex] + currentIndex++ + } + + return newSlice +} diff --git a/lsp/capabilities/sync/sync_test.go b/lsp/capabilities/sync/sync_test.go new file mode 100644 index 00000000..3a257af9 --- /dev/null +++ b/lsp/capabilities/sync/sync_test.go @@ -0,0 +1,73 @@ +package sync_test + +import ( + "testing" + + "github.com/hephbuild/heph/lsp/capabilities/sync" + "github.com/stretchr/testify/require" +) + +func TestByteInsert(t *testing.T) { + // UTF-8 + currText := "def hello_world():" + newText := "_my_precious" + currBytes := []byte(currText) + newBytes := []byte(newText) + insertedArray := sync.ParseNewBytes(currBytes, newBytes, 15, 15) + + expectedText := "def hello_world_my_precious():" + actualText := string(insertedArray) + require.Equal(t, expectedText, actualText) +} + +func TestByteInsertWithNewLine(t *testing.T) { + // UTF-8 + currText := "def hello_world():" + newText := "_my_precious\n" + currBytes := []byte(currText) + newBytes := []byte(newText) + insertedArray := sync.ParseNewBytes(currBytes, newBytes, 15, 15) + + expectedText := "def hello_world_my_precious\n():" + actualText := string(insertedArray) + require.Equal(t, expectedText, actualText) +} + +func TestByteReplace(t *testing.T) { + // UTF-8 + currText := "def hello_world():" + newText := "world_hello" + currBytes := []byte(currText) + newBytes := []byte(newText) + insertedArray := sync.ParseNewBytes(currBytes, newBytes, 4, 4+len(newBytes)) + + expectedText := "def world_hello():" + actualText := string(insertedArray) + require.Equal(t, expectedText, actualText) +} + +func TestByteReplaceWithInsert(t *testing.T) { + // UTF-8 + currText := "def hello_friends():" + newText := "hi_world" + currBytes := []byte(currText) + newBytes := []byte(newText) + insertedArray := sync.ParseNewBytes(currBytes, newBytes, 4, 9) + + expectedText := "def hi_world_friends():" + actualText := string(insertedArray) + require.Equal(t, expectedText, actualText) +} + +func TestByteRemove(t *testing.T) { + // UTF-8 + currText := "def hello_world():" + newText := "" + currBytes := []byte(currText) + newBytes := []byte(newText) + insertedArray := sync.ParseNewBytes(currBytes, newBytes, 4, 10) + + expectedText := "def world():" + actualText := string(insertedArray) + require.Equal(t, expectedText, actualText) +} diff --git a/lsp/capabilities/sync/workspace.go b/lsp/capabilities/sync/workspace.go new file mode 100644 index 00000000..89ae14d1 --- /dev/null +++ b/lsp/capabilities/sync/workspace.go @@ -0,0 +1,27 @@ +package sync + +import ( + "github.com/hephbuild/heph/lsp/runtime" + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func WorkspaceDidRenameFilesFunc(manager *runtime.Manager) protocol.WorkspaceDidRenameFilesFunc { + return func(context *glsp.Context, params *protocol.RenameFilesParams) error { + for _, file := range params.Files { + manager.RenameDocument(file.OldURI, file.NewURI) + } + + return nil + } +} + +func WorkspaceDidDeleteFilesFunc(manager *runtime.Manager) protocol.WorkspaceDidDeleteFilesFunc { + return func(context *glsp.Context, params *protocol.DeleteFilesParams) error { + for _, file := range params.Files { + manager.DeleteDocument(file.URI) + } + + return nil + } +} diff --git a/lsp/runtime/builtin/builtin.go b/lsp/runtime/builtin/builtin.go new file mode 100644 index 00000000..e1b3d4a6 --- /dev/null +++ b/lsp/runtime/builtin/builtin.go @@ -0,0 +1,50 @@ +package builtin + +import ( + _ "embed" + "slices" + + "github.com/hephbuild/heph/lsp/runtime/query" + "github.com/hephbuild/heph/lsp/runtime/symbol" + tree_sitter "github.com/tree-sitter/go-tree-sitter" +) + +//go:embed target.py +var target []byte + +//go:embed helpers.py +var helpers []byte + +//go:embed pybt.py +var pybt []byte + +const builtinSource = "hbuiltins" + +// ParseBuiltins Parses builtin files and extracts their symbols. +func ParseBuiltins(parser *tree_sitter.Parser) ([]*symbol.Symbol, error) { + targetTree := parser.Parse(target, nil) + defer targetTree.Close() + + helpersTree := parser.Parse(helpers, nil) + defer helpersTree.Close() + + pybtTree := parser.Parse(pybt, nil) + defer pybtTree.Close() + + targetSymbols, err := query.QuerySymbols(targetTree, target, builtinSource) + if err != nil { + return nil, err + } + + helpersSymbols, err := query.QuerySymbols(helpersTree, helpers, builtinSource) + if err != nil { + return nil, err + } + + pybtSymbols, err := query.QuerySymbols(pybtTree, pybt, builtinSource) + if err != nil { + return nil, err + } + + return slices.Concat(targetSymbols, helpersSymbols, pybtSymbols), nil +} diff --git a/lsp/runtime/builtin/builtin_test.go b/lsp/runtime/builtin/builtin_test.go new file mode 100644 index 00000000..9d0b3718 --- /dev/null +++ b/lsp/runtime/builtin/builtin_test.go @@ -0,0 +1,59 @@ +package builtin_test + +import ( + "testing" + + "github.com/hephbuild/heph/lsp/runtime/builtin" + "github.com/stretchr/testify/suite" + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" +) + +var allDefs = []string{ + // From helpers.py + "text_file", "json_file", "tool_target", "group", "load", + + // From pybt.py - Top-level functions + "abs", "any", "all", "bool", "chr", "dict", "dir", + "enumerate", "fail", "float", "getattr", "hasattr", + "hash", "int", "len", "list", "max", "min", "ord", + "print", "range", "repr", "reversed", "set", "sorted", + "str", "tuple", "type", "zip", + + // From target.py + "target", +} + +type BuiltinSuite struct { + suite.Suite +} + +func (suite *BuiltinSuite) newParser() *tree_sitter.Parser { + parser := tree_sitter.NewParser() + + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + suite.Require().NoError(err) + + return parser +} + +func (suite *BuiltinSuite) TestSymbols() { + parser := suite.newParser() + symbols, err := builtin.ParseBuiltins(parser) + suite.Require().NoError(err) + + suite.Require().NotNil(symbols) + suite.Require().NotEmpty(symbols) + + // Extract symbol names for comparison + symbolNames := make([]string, 0, len(symbols)) + for _, sym := range symbols { + symbolNames = append(symbolNames, sym.Name) + } + + suite.Require().ElementsMatch(allDefs, symbolNames) +} + +func TestTBuiltinSuite(t *testing.T) { + suite.Run(t, &BuiltinSuite{}) +} diff --git a/lsp/runtime/builtin/common.go b/lsp/runtime/builtin/common.go new file mode 100644 index 00000000..b00e6d6c --- /dev/null +++ b/lsp/runtime/builtin/common.go @@ -0,0 +1,6 @@ +package builtin + +// Builtins names +const ( + LoadName = "load" +) diff --git a/lsp/runtime/builtin/helpers.py b/lsp/runtime/builtin/helpers.py new file mode 100644 index 00000000..5e3c2c26 --- /dev/null +++ b/lsp/runtime/builtin/helpers.py @@ -0,0 +1,71 @@ +from typing import Union, List, Optional, Literal, Any, Dict + + +def text_file( + name: str, + out: str, + text: str, +): + """Creates a target which outputs a text file + + Args: + name (str): Target name (required) + out (str): Output file path + text (str): Text content to write to the file + """ + pass + + +def json_file( + name: str, + out: str, + data: Dict[str, Any], +): + """Creates a target which outputs a json file from a Starlark object + + Args: + name (str): Target name (required) + out (str): Output file path + data (Dict[str, Any]): JSON data to write to the file + """ + pass + + +def tool_target( + name: str, + tools: Union[str, List[str], dict], +): + """Creates a target which will proxy invocation to a binary + + This is useful to allow execution of binaries from the cli + + Args: + name (str): Target name (required) + tools (Union[str, List[str], dict]): Tools to be exposed to this target + (available in `PATH`) + """ + pass + + +def group( + name: str, + deps: Union[str, List[str], dict], +): + """Creates a target which only collects & outputs a set of files + + Args: + name (str): Target name (required) + deps (Union[str, List[str], dict]): Dependencies required by this target + (target and files), will be copied to the sandbox and part of the hash + """ + pass + + +def load(name: str, *funs: str): + """ + Loads Heph script from path. Need to load at least one function. + Args: + name (str): Heph BUILD path without file + funs (str): List of functions loaded + """ + pass diff --git a/lsp/runtime/builtin/pybt.py b/lsp/runtime/builtin/pybt.py new file mode 100644 index 00000000..c13796be --- /dev/null +++ b/lsp/runtime/builtin/pybt.py @@ -0,0 +1,116 @@ +def abs(x): + """`abs(x)` returns the absolute value of its argument `x`, which must be an int or float. The result has the same type as `x`.""" + pass + +def any(x) -> bool: + """`any(x)` returns `True` if any element of the iterable sequence x has a truth value of true. If the iterable is empty, it returns `False`.""" + pass + +def all(x) -> bool: + """`all(x)` returns `False` if any element of the iterable sequence x has a truth value of false. If the iterable is empty, it returns `True`.""" + pass + +def bool(x) -> bool: + """`bool(x)` interprets `x` as a Boolean value---`True` or `False`. With no argument, `bool()` returns `False`.""" + pass + +def chr(i): + """`chr(i)` returns a string that encodes the single Unicode code point whose value is specified by the integer `i`. `chr` fails unless 0 ≤ `i` ≤ 0x10FFFF.""" + pass + +def dict() -> Dict: + """`dict` creates a dictionary. It accepts up to one positional argument, which is interpreted as an iterable of two-element sequences (pairs), each specifying a key/value pair in the resulting dictionary.""" + pass + +def dir(x) -> List[String]: + """`dir(x)` returns a new sorted list of the names of the attributes (fields and methods) of its operand. The attributes of a value `x` are the names `f` such that `x.f` is a valid expression.""" + pass + +def enumerate(x) -> List[Tuple[int, any]]: + """`enumerate(x)` returns a list of (index, value) pairs, each containing successive values of the iterable sequence xand the index of the value within the sequence.""" + pass + +def fail(*args, sep=" "): + """The `fail(*args, sep=" ")` function causes execution to fail with the specified error message. Like `print`, arguments are formatted as if by `str(x)` and separated by a space, unless an alternative separator is specified by a `sep` named argument.""" + pass + +def float(x) -> float: + """`float(x)` interprets its argument as a floating-point number.""" + pass + +def getattr(x, name): + """`getattr(x, name)` returns the value of the attribute (field or method) of x named `name`. It is a dynamic error if x has no such attribute.""" + pass + +def hasattr(x, name) -> bool: + """`hasattr(x, name)` reports whether x has an attribute (field or method) named `name`.""" + pass + +def hash(x) -> int: + """`hash(x)` returns an integer hash of a string x such that two equal strings have the same hash. In other words `x == y` implies `hash(x) == hash(y)`.""" + pass + +def int(x) -> int: + """`int(x[, base])` interprets its argument as an integer.""" + pass + +def len(x) -> int: + """`len(x)` returns the number of elements in its argument.""" + pass + +def list() -> List: + """`list` constructs a list.""" + pass + +def max(x): + """`max(x)` returns the greatest element in the iterable sequence x.""" + pass + +def min(x): + """`min(x)` returns the least element in the iterable sequence x.""" + pass + +def ord(s): + """`ord(s)` returns the integer value of the sole Unicode code point encoded by the string `s`.""" + pass + +def print(*args, sep=" "): + """`print(*args, sep=" ")` prints its arguments, followed by a newline. Arguments are formatted as if by `str(x)` and separated with a space, unless an alternative separator is specified by a `sep` named argument.""" + pass + +def range() -> List[int]: + """`range` returns an immutable sequence of integers defined by the specified interval and stride.""" + pass + +def repr(x) -> String: + """`repr(x)` formats its argument as a string.""" + pass + +def reversed(x) -> List: + """`reversed(x)` returns a new list containing the elements of the iterable sequence x in reverse order.""" + pass + +def set(x): + """`set(x)` returns a new set containing the elements of the iterable x. With no argument, `set()` returns a new empty set.""" + pass + +def sorted(x) -> List: + """`sorted(x)` returns a new list containing the elements of the iterable sequence x, in sorted order. The sort algorithm is stable.""" + pass + +def str(x) -> String: + """`str(x)` formats its argument as a string.""" + pass + +def tuple(x): + """`tuple(x)` returns a tuple containing the elements of the iterable x.""" + pass + +def type(x) -> String: + """type(x) returns a string describing the type of its operand.""" + pass + +def zip() -> List: + """`zip()` returns a new list of n-tuples formed from corresponding elements of each of the n iterable sequences provided as arguments to `zip`. That is, the first tuple contains the first element of each of the sequences, the second element contains the second element of each of the sequences, and so on. The result list is only as long as the shortest of the input sequences.""" + pass + diff --git a/lsp/runtime/builtin/target.py b/lsp/runtime/builtin/target.py new file mode 100644 index 00000000..1cce05f7 --- /dev/null +++ b/lsp/runtime/builtin/target.py @@ -0,0 +1,89 @@ +from typing import Union, List, Optional, Literal, Any + + +def target( + name: str, + doc: str = None, + run: Union[str, List[str]] = [], + entrypoint: Literal["bash", "sh", "exec"] = "bash", + run_in_cwd: bool = False, + pass_args: bool = False, + cache: Union[bool, Any] = True, + support_files: Union[str, List[str]] = [], + sandbox: bool = True, + out_in_sandbox: bool = False, + gen: bool = False, + codegen: Optional[Literal["link", "copy"]] = None, + deps: Union[str, List[str], dict] = [], + hash_deps: Union[str, List[str], dict] = [], + runtime_deps: Union[str, List[str], dict] = [], + tools: Union[str, List[str], dict] = [], + labels: Union[str, List[str]] = [], + out: Union[str, List[str], dict] = [], + env: dict = {}, + pass_env: List[str] = [], + src_env: Literal["ignore", "rel_root", "rel_pkg", "abs"] = "rel_pkg", + out_env: Literal["ignore", "rel_root", "rel_pkg", "abs"] = "rel_pkg", + hash_file: Literal["content", "mod_time"] = "content", + transitive: Any = None, + timeout: str = None, +): + """Define a target for execution in the heph build system. + + A target is defined by a name, a set of commands to run, a set of inputs and outputs + and environment variables. This execution unit is isolated from the rest of the repo + which allows for efficient caching and parallel execution. + + Args: + name (str): Target name (required) + doc (str, optional): Documentation for the target + run (Union[str, List[str]], optional): Command(s) to run (see `executor`) + entrypoint (Literal['bash', 'sh', 'exec'], optional): How to execute the run commands. + - 'bash', 'sh': runs commands with `bash -c` or `sh -c` (each array item on new line) + - 'exec': uses `run` as array of arguments passed to `exec` + run_in_cwd (bool, optional): Will run the target in the current working directory, + use with `sandbox=False` + pass_args (bool, optional): Forward extra args passed to heph to the command + (ex: `heph run //some/target -- arg1 arg2`) + cache (Union[bool, Any], optional): Cache configuration. + - `bool`: enabled or disables cache, will cache the paths defined in `out` + - `heph.cache()`: named cache configuration + support_files (Union[str, List[str]], optional): Files to be cached, but not part of `out`. + Useful for support files used by a tool during runtime + sandbox (bool, optional): Enables sandbox. heph creates a directory (where target cwd is set), + copies `deps`, overrides `PATH` with needed `tools`, and only exposes environment + variables defined by `env` and `pass_env` + out_in_sandbox (bool, optional): Will collect output from the sandbox when sandboxing + is disabled, use with `sandbox=False` + gen (bool, optional): Marks target as a generating target + codegen (Optional[Literal['link', 'copy']], optional): Enables linking output back into tree, + through symlink or hard copy + deps (Union[str, List[str], dict], optional): Dependencies required by this target + (target and files), will be copied to the sandbox and part of the hash + hash_deps (Union[str, List[str], dict], optional): Dependencies used only to compute the + target hash, they will not be copied to the sandbox + runtime_deps (Union[str, List[str], dict], optional): Dependencies required by this target, + will be copied to the sandbox, but will not be part of the hash, use with caution + tools (Union[str, List[str], dict], optional): Tools to be exposed to this target + (available in `PATH`) + labels (Union[str, List[str]], optional): Labels for this target + out (Union[str, List[str], dict], optional): Output files for this target, supports glob + env (dict, optional): Key/value pairs of environment variables set in the sandbox + pass_env (List[str], optional): Environment variable names to be passed from the + outside environment + src_env (Literal['ignore', 'rel_root', 'rel_pkg', 'abs'], optional): How to expose + dependency paths as environment variables inside the sandbox. + For dependencies: `SRC_` for named deps, or just `SRC`. + Default exposes path relative to the package + out_env (Literal['ignore', 'rel_root', 'rel_pkg', 'abs'], optional): How to expose + output paths as environment variables inside the sandbox. + For output: `OUT_` for named output, or just `OUT`. + Default exposes path relative to the package + hash_file (Literal['content', 'mod_time'], optional): Method to hash dependencies + transitive (Any, optional): Optional specification of `deps`, `tools`, `env` and `pass_env` + that will be transitively applied when the current target is required as `tools` or `deps`. + Useful if a tool requires another tool to function at runtime + timeout (str, optional): Timeout to run target + """ + pass + diff --git a/lsp/runtime/document/document.go b/lsp/runtime/document/document.go new file mode 100644 index 00000000..e53257e7 --- /dev/null +++ b/lsp/runtime/document/document.go @@ -0,0 +1,260 @@ +package document + +import ( + "strings" + "sync" + "unsafe" + + "github.com/hephbuild/heph/lsp/runtime/builtin" + "github.com/hephbuild/heph/lsp/runtime/query" + "github.com/hephbuild/heph/lsp/runtime/symbol" + tree_sitter "github.com/tree-sitter/go-tree-sitter" +) + +type Document struct { + // FullPath + FullPath string + + Symbols []*symbol.Symbol + Calls []*symbol.Symbol + + // Loads are the BUILD path, + // if any file has it name changed or delete, + // Loads are not updated until next file sync, so use DocLoads + Loads []*RawLoad + DocLoads map[string]*Load + IsLoadedBy map[string]*Load + + Tree *tree_sitter.Tree + Text []byte // UTF-16 encoded byte array https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#textDocuments + TextString string // UTF-16 encoded string https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#textDocuments + + treeMutex sync.Mutex + loadsMu sync.RWMutex +} + +type RawLoad struct { + Path string + Loads []string +} + +type Load struct { + Doc *Document + Loads []string +} + +func (l *Load) Hash() string { + return l.Doc.FullPath + "|" + strings.Join(l.Loads, "|") +} + +// lockTwoLoads locks both docs mutexes in a deterministic way. Ugly, but keep the caller from undestading how call the locks +func lockTwoLoads(mu1, mu2 *sync.RWMutex) { + p1 := uintptr(unsafe.Pointer(mu1)) + p2 := uintptr(unsafe.Pointer(mu2)) + if p1 < p2 { + mu1.Lock() + mu2.Lock() + } else { + mu2.Lock() + mu1.Lock() + } +} + +func unlockTwoLoads(mu1, mu2 *sync.RWMutex) { + mu1.Unlock() + mu2.Unlock() +} + +func (d *Document) Close() { + d.Tree.Close() +} + +func NewDocument(name string, tree *tree_sitter.Tree, rawText []byte) (*Document, error) { + doc := &Document{ + FullPath: name, Tree: tree, Text: rawText, TextString: string(rawText), + DocLoads: make(map[string]*Load), IsLoadedBy: make(map[string]*Load), + } + + syms, calls, err := extractSymbols(doc.Tree, doc.Text, doc.FullPath) + doc.Symbols = syms + doc.Calls = calls + doc.extractLoads() + + return doc, err +} + +// SwapTree atomic swaps current tree and return the closed old tree +func (d *Document) SwapTree(newT *tree_sitter.Tree, newText []byte) (*tree_sitter.Tree, error) { + d.treeMutex.Lock() + defer d.treeMutex.Unlock() + + oldTree := d.Tree + + syms, calls, err := extractSymbols(newT, newText, d.FullPath) + if err != nil { + return nil, err + } + + d.Symbols = syms + d.Calls = calls + d.Tree = newT + d.Text = newText + d.TextString = string(newText) + + d.resetLoads() + d.extractLoads() + + oldTree.Close() + + return oldTree, err +} + +func extractSymbols(tree *tree_sitter.Tree, text []byte, source string) ([]*symbol.Symbol, []*symbol.Symbol, error) { + symbols, err := query.QuerySymbols(tree, text, source) + if err != nil { + return nil, nil, err + } + + calls, err := query.QueryCalls(tree, text, source) + + return symbols, calls, err +} + +func (d *Document) extractLoads() { + loads := []*RawLoad{} + for _, call := range d.Calls { + if call.Name == builtin.LoadName { + if len(call.Parameters) < 2 { + continue + } + + rawValue := call.Parameters[0].Value + path := strings.Trim(rawValue, "\"") + loadsSlice := []string{} + for i := 1; i < len(call.Parameters); i++ { + rawFunction := call.Parameters[i].Value + fun := strings.Trim(rawFunction, "\"") + loadsSlice = append(loadsSlice, fun) + } + loads = append(loads, &RawLoad{Path: path, Loads: loadsSlice}) + } + } + + d.Loads = loads +} + +func (d *Document) ExtractCurrentStringLiteral(byteOffSet uint) string { + return query.ExtractCurrentStringLiteral(d.Tree.RootNode(), d.Text, byteOffSet) +} + +func (d *Document) ExtractCurrentSymbol(byteOffSet uint) (*symbol.Symbol, bool) { + sName := d.ExtractCurrentSymbolName(byteOffSet) + return d.Query(sName) +} + +func (d *Document) ExtractCurrentSymbolName(byteOffSet uint) string { + return query.ExtractCurrentSymbol(d.Tree.RootNode(), d.Text, byteOffSet) +} + +func (d *Document) ExtractCurrentFunctionName(byteOffSet uint) string { + return query.ExtractFunctionNameFromOffset(d.Tree.RootNode(), d.Text, byteOffSet) +} + +func (d *Document) Query(symbolName string) (*symbol.Symbol, bool) { + return symbol.FindSymbol(d.Symbols, symbolName) +} + +func (d *Document) QueryMany(symbolName []string) (*symbol.Symbol, bool) { + return symbol.FindManySymbol(d.Symbols, symbolName) +} + +func (d *Document) QueryAll(symbolName []string) []*symbol.Symbol { + return symbol.FindManySymbols(d.Symbols, symbolName) +} + +func (d *Document) QueryCalls(symbolName string) []*symbol.Symbol { + return symbol.FindCalls(d.Calls, symbolName) +} + +func (d *Document) RangeDocLoads(fn func(*Load)) { + d.loadsMu.RLock() + defer d.loadsMu.RUnlock() + for _, doc := range d.DocLoads { + fn(doc) + } +} + +func (d *Document) RangeIsLoadedBy(fn func(*Load)) { + d.loadsMu.RLock() + defer d.loadsMu.RUnlock() + for _, doc := range d.IsLoadedBy { + fn(doc) + } +} + +func (d *Document) resetLoads() { + // Copy to remove from LoadedBy + d.loadsMu.RLock() + docs := make([]*Load, 0, len(d.DocLoads)) + for _, load := range d.DocLoads { + docs = append(docs, load) + } + d.loadsMu.RUnlock() + + for _, load := range docs { + doc := load.Doc + doc.RemoveLoadedByDoc(d) + } + + d.loadsMu.Lock() + d.DocLoads = make(map[string]*Load) + d.loadsMu.Unlock() +} + +func (d *Document) AddLoadedDoc(doc *Document, loads []string) { + // Locks both documents + lockTwoLoads(&d.loadsMu, &doc.loadsMu) + + dl := &Load{Doc: doc, Loads: loads} + d.DocLoads[dl.Hash()] = dl + + dlby := &Load{Doc: d, Loads: loads} + doc.IsLoadedBy[dlby.Hash()] = dlby + + unlockTwoLoads(&d.loadsMu, &doc.loadsMu) +} + +func (d *Document) RemoveLoadedDoc(doc *Document) { + lockTwoLoads(&d.loadsMu, &doc.loadsMu) + + // remove from d.DocLoads + for key, load := range d.DocLoads { + if load.Doc == doc { + delete(d.DocLoads, key) + break + } + } + + // remove from doc.IsLoadedBy + for key, load := range doc.IsLoadedBy { + if load.Doc == d { + delete(doc.IsLoadedBy, key) + break + } + } + + unlockTwoLoads(&d.loadsMu, &doc.loadsMu) +} + +func (d *Document) RemoveLoadedByDoc(doc *Document) { + doc.loadsMu.Lock() + + for key, loader := range doc.IsLoadedBy { + if loader.Doc == d { + delete(doc.IsLoadedBy, key) + break + } + } + + doc.loadsMu.Unlock() +} diff --git a/lsp/runtime/document/document_test.go b/lsp/runtime/document/document_test.go new file mode 100644 index 00000000..877c1713 --- /dev/null +++ b/lsp/runtime/document/document_test.go @@ -0,0 +1,110 @@ +package document_test + +import ( + _ "embed" + "testing" + + "github.com/hephbuild/heph/lsp/runtime/document" + "github.com/hephbuild/heph/lsp/runtime/symbol" + "github.com/stretchr/testify/suite" + + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" +) + +//go:embed testdata/test_document.py +var pythonTestFile []byte + +type DocumentTestSuite struct { + suite.Suite +} + +func (s *DocumentTestSuite) TestNewDocument() { + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse(pythonTestFile, nil) + s.Require().NotNil(tree) + + doc, err := document.NewDocument("test.py", tree, pythonTestFile) + s.Require().NoError(err) + s.Require().NotNil(doc) + + s.Equal("test.py", doc.FullPath) + s.NotNil(doc.Tree) + s.NotNil(doc.Text) + s.NotNil(doc.Symbols) + s.NotEmpty(doc.Symbols) +} + +func (s *DocumentTestSuite) TestSwapTree() { + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse(pythonTestFile, nil) + s.Require().NotNil(tree) + + doc, err := document.NewDocument("test.py", tree, pythonTestFile) + s.Require().NoError(err) + s.Require().NotNil(doc) + + initialSymbolCount := len(doc.Symbols) + s.True(initialSymbolCount > 0, "Should have some symbols") + + newContent := []byte(`def new_function(): + return "new" + +new_variable = 42`) + + newTree := parser.Parse(newContent, nil) + s.Require().NotNil(newTree) + + oldTree, err := doc.SwapTree(newTree, newContent) + s.Require().NoError(err) + s.Require().NotNil(oldTree) + + s.Equal(string(newContent), doc.TextString) + s.NotEmpty(doc.Symbols) + + foundSymbols := make([]string, 0, len(doc.Symbols)) + for _, sym := range doc.Symbols { + foundSymbols = append(foundSymbols, sym.Name) + } + + s.Contains(foundSymbols, "new_variable", "Should contain new variable") +} + +func (s *DocumentTestSuite) TestQuery() { + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse(pythonTestFile, nil) + s.Require().NotNil(tree) + + doc, err := document.NewDocument("test.py", tree, pythonTestFile) + s.Require().NoError(err) + s.Require().NotNil(doc) + + sym, found := doc.Query("another_function") + s.True(found, "Should find another_function") + s.Require().NotNil(sym) + s.Equal("another_function", sym.Name) + s.Equal(symbol.FunctionKind, sym.Kind) + + sym, found = doc.Query("string_literal") + s.True(found, "Should find string_literal") + s.Require().NotNil(sym) + s.Equal("string_literal", sym.Name) + s.Equal(symbol.VariableKind, sym.Kind) + + sym, found = doc.Query("nonexistent") + s.False(found, "Should not find nonexistent symbol") + s.Nil(sym) +} + +func TestDocumentTestSuite(t *testing.T) { + suite.Run(t, new(DocumentTestSuite)) +} diff --git a/lsp/runtime/document/testdata/test_document.py b/lsp/runtime/document/testdata/test_document.py new file mode 100644 index 00000000..73e7f55d --- /dev/null +++ b/lsp/runtime/document/testdata/test_document.py @@ -0,0 +1,18 @@ +"""Test Python file for document tests.""" + +def hello_world(): + """A simple function.""" + return "Hello, World!" + +def another_function(x, y): + """Another function with calculations.""" + z = x * y + my_var = 42 + return z + my_var + +string_literal = "//heph/string/literal" + +my_variable = "test" +another_var = 123 +calculation = another_function(3, 4) + diff --git a/lsp/runtime/manager.go b/lsp/runtime/manager.go new file mode 100644 index 00000000..f10abcac --- /dev/null +++ b/lsp/runtime/manager.go @@ -0,0 +1,321 @@ +package runtime + +import ( + "os" + "path" + "slices" + "strings" + "sync" + + "github.com/hephbuild/heph/lsp/runtime/builtin" + "github.com/hephbuild/heph/lsp/runtime/document" + "github.com/hephbuild/heph/lsp/runtime/symbol" + tree_sitter "github.com/tree-sitter/go-tree-sitter" +) + +const HephLanguage = "heph" + +var Version = "0.0.1" + +type docTuple struct { + Document *document.Document + + // Last reported version + version int32 +} + +type Manager struct { + DocumentMap sync.Map + + BuiltinSymbols []*symbol.Symbol + Parser *tree_sitter.Parser + + WorkspaceFolder string +} + +func NewManager(parser *tree_sitter.Parser) (*Manager, error) { + builtins, err := builtin.ParseBuiltins(parser) + if err != nil { + return nil, err + } + + return &Manager{DocumentMap: sync.Map{}, Parser: parser, BuiltinSymbols: builtins}, nil +} + +// GetDocument queries and look for an existing document in Manager. +// returns nil if not present +func (m *Manager) GetDocument(uri string) (*document.Document, bool) { + uri = normalizeDocName(uri) + if val, ok := m.DocumentMap.Load(uri); ok { + tuple := val.(*docTuple) + return tuple.Document, true + } + + return nil, false +} + +func (m *Manager) NewDocument(name string, tree *tree_sitter.Tree, rawText []byte, version int32) (*document.Document, error) { + name = normalizeDocName(name) + newDoc, err := document.NewDocument(name, tree, rawText) + if err != nil { + return nil, err + } + + m.setDocument(name, version, newDoc) + + // Load all other documents from all newDoc.Loads + // go m.loadDocumentsFromLoads(newDoc) + m.loadDocumentsFromLoads(newDoc) + + return newDoc, nil +} + +func (m *Manager) SwapTree(doc *document.Document, newT *tree_sitter.Tree, newText []byte) (*tree_sitter.Tree, error) { + t, err := doc.SwapTree(newT, newText) + if err != nil { + return nil, err + } + + // Load all other documents from all doc.Loads + // go m.loadDocumentsFromLoads(doc) + m.loadDocumentsFromLoads(doc) + + return t, nil +} + +func (m *Manager) loadDocumentsFromLoads(doc *document.Document) { + for _, rawLoad := range doc.Loads { + loadPath := rawLoad.Path + if loadPath == "" { + continue + } + + // Convert load path to folder path + folderPath := path.Join(m.WorkspaceFolder, loadPath) + + // Read directory + entries, err := os.ReadDir(folderPath) + if err != nil { + continue + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + if !strings.Contains(entry.Name(), "BUILD") { + continue + } + + filePath := path.Join(folderPath, entry.Name()) + + normalizedName := normalizeDocName(filePath) + + // If tries to load itself + if normalizedName == doc.FullPath { + continue + } + + // Skip if already loaded + if fDoc, found := m.GetDocument(normalizedName); found { + doc.AddLoadedDoc(fDoc, rawLoad.Loads) + continue + } + + // Read file content + content, err := os.ReadFile(filePath) + if err != nil { + continue + } + + // Parse the file + tree := m.Parser.Parse(content, nil) + if tree == nil { + continue + } + + // Create new document and ignore if there's an error + newDoc, err := document.NewDocument(normalizedName, tree, content) + if err != nil { + tree.Close() + continue + } + + // Cross ref + doc.AddLoadedDoc(newDoc, rawLoad.Loads) + + m.setDocument(normalizedName, 0, newDoc) + + // Recursively load its loads + m.loadDocumentsFromLoads(newDoc) + } + } +} + +func (m *Manager) setDocument(uri string, version int32, doc *document.Document) { + if val, ok := m.DocumentMap.Load(uri); ok { + tuple := val.(*docTuple) + tuple.Document.Close() + + tuple.Document = doc + tuple.version = version + } else { + m.DocumentMap.Store(uri, &docTuple{ + Document: doc, + version: version, + }) + } +} + +type Filter func(s *symbol.Symbol) bool + +func (m *Manager) AllLoadedSymbols(filters ...Filter) []*symbol.Symbol { + allSymbols := m.BuiltinSymbols + m.DocumentMap.Range(func(key, value any) bool { + doc := value.(*docTuple) + for _, smb := range doc.Document.Symbols { + shoulAdd := true + for _, f := range filters { + if !f(smb) { + shoulAdd = false + } + } + + if shoulAdd { + allSymbols = append(allSymbols, smb) + } + + } + return true + }) + + return allSymbols +} + +// kindStruct is a struct helper to get symbol per kind +type kindStruct struct { + AllSymbols []*symbol.Symbol + Variables []*symbol.Symbol + Functions []*symbol.Symbol +} + +func (m *Manager) AllLoadedSymbolsPerKind() kindStruct { + allSymbols := []*symbol.Symbol{} + vars := []*symbol.Symbol{} + funs := []*symbol.Symbol{} + + for _, smb := range m.BuiltinSymbols { + switch smb.Kind { + case symbol.FunctionKind: + funs = append(funs, smb) + case symbol.VariableKind: + vars = append(vars, smb) + default: + allSymbols = append(allSymbols, smb) + } + } + + m.DocumentMap.Range(func(key, value any) bool { + doc := value.(*docTuple) + for _, smb := range doc.Document.Symbols { + switch smb.Kind { + case symbol.FunctionKind: + funs = append(funs, smb) + case symbol.VariableKind: + vars = append(vars, smb) + default: + allSymbols = append(allSymbols, smb) + } + } + return true + }) + + allSymbols = slices.Concat(allSymbols, vars, funs) + + return kindStruct{ + AllSymbols: allSymbols, + Variables: vars, + Functions: funs, + } +} + +func (m *Manager) Query(symbolName string) (*symbol.Symbol, bool) { + if s, found := symbol.FindSymbol(m.BuiltinSymbols, symbolName); found { + return s, true + } + + if _, s, found := m.QueryDoc(symbolName); found { + return s, found + } + + return nil, false +} + +func (m *Manager) QueryDoc(symbolName string) (*document.Document, *symbol.Symbol, bool) { + var foundDoc *document.Document + var foundSymbol *symbol.Symbol + m.DocumentMap.Range(func(key, value any) bool { + doc := value.(*docTuple) + if s, found := doc.Document.Query(symbolName); found { + foundDoc = doc.Document + foundSymbol = s + + return false + } + + return true + }) + if foundDoc != nil { + return foundDoc, foundSymbol, true + } + + return nil, nil, false +} + +func (m *Manager) QueryBuiltin(symbolName string) (*symbol.Symbol, bool) { + return symbol.FindSymbol(m.BuiltinSymbols, symbolName) +} + +type callQueryResult struct { + Doc *document.Document + Symbols []*symbol.Symbol +} + +func (m *Manager) QueryCallsDoc(symbolName string) []callQueryResult { + res := []callQueryResult{} + m.DocumentMap.Range(func(key, value any) bool { + doc := value.(*docTuple) + if calls := doc.Document.QueryCalls(symbolName); len(calls) > 0 { + res = append(res, callQueryResult{doc.Document, calls}) + } + + return true + }) + + return res +} + +func (m *Manager) DeleteDocument(uri string) { + uri = normalizeDocName(uri) + if val, ok := m.DocumentMap.LoadAndDelete(uri); ok { + tuple := val.(*docTuple) + tuple.Document.Close() + } +} + +func (m *Manager) RenameDocument(oldURI string, newURI string) { + oldURI = normalizeDocName(oldURI) + newURI = normalizeDocName(newURI) + if val, ok := m.DocumentMap.LoadAndDelete(oldURI); ok { + tuple := val.(*docTuple) + tuple.Document.FullPath = newURI + m.DocumentMap.Store(newURI, tuple) + } +} + +func normalizeDocName(uri string) string { + uri = strings.TrimPrefix(uri, "file://") + + return uri +} diff --git a/lsp/runtime/query/call.go b/lsp/runtime/query/call.go new file mode 100644 index 00000000..4b7a2b5c --- /dev/null +++ b/lsp/runtime/query/call.go @@ -0,0 +1,69 @@ +package query + +import ( + "maps" + "slices" + "strconv" + + "github.com/hephbuild/heph/lsp/runtime/symbol" + tree_sitter "github.com/tree-sitter/go-tree-sitter" +) + +const callQuery = ` +(call function: (identifier) @call.name . (argument_list (_) @call.arg)? ) @call.stmt +` + +func QueryCalls(tree *tree_sitter.Tree, text []byte, source string) ([]*symbol.Symbol, error) { + if tree.RootNode() == nil { + return nil, ErrEmptyTreeError + } + + query, err := tree_sitter.NewQuery(lang, callQuery) + if err != nil { + return nil, err + } + defer query.Close() + + cursor := tree_sitter.NewQueryCursor() + defer cursor.Close() + + matches := cursor.Matches(query, tree.RootNode(), text) + funs := map[uintptr]*symbol.Symbol{} + + for match := matches.Next(); match != nil; match = matches.Next() { + currSymbol := &symbol.Symbol{Kind: symbol.FunctionCallKind, Source: source} + for _, capture := range match.Captures { + currentNode := &capture.Node + patternName := query.CaptureNames()[capture.Index] + nodeRange := currentNode.Range() + patternValue := currentNode.Utf8Text(text) + + switch patternName { + case "call.name": + // Params query repeats Captures. We use NodeId so we dont need to make multiple queries + if ss, ok := funs[currentNode.Id()]; ok { + ss.Parameters = append(ss.Parameters, currSymbol.Parameters...) + currSymbol = ss + } + + currSymbol.Name = patternValue + currSymbol.FullyQualifiedName = patternValue + currSymbol.Position.RowStart = nodeRange.StartPoint.Row + currSymbol.Position.ColumnStart = nodeRange.StartPoint.Column + + funs[currentNode.Id()] = currSymbol + case "call.arg": + newParam := symbol.Parameter{Name: strconv.Itoa(len(currSymbol.Parameters)), Value: patternValue} + currSymbol.Parameters = append(currSymbol.Parameters, &newParam) + + // Last pattern + currSymbol.Position.RowEnd = nodeRange.EndPoint.Row + currSymbol.Position.ColumnEnd = nodeRange.EndPoint.Column + case "call.stmt": + currSymbol.Signature = patternValue + } + } + } + + return slices.Collect(maps.Values(funs)), nil +} diff --git a/lsp/runtime/query/call_test.go b/lsp/runtime/query/call_test.go new file mode 100644 index 00000000..e36581ee --- /dev/null +++ b/lsp/runtime/query/call_test.go @@ -0,0 +1,128 @@ +package query_test + +import ( + _ "embed" + "testing" + + "github.com/hephbuild/heph/lsp/runtime/query" + "github.com/hephbuild/heph/lsp/runtime/symbol" + "github.com/stretchr/testify/suite" + + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" +) + +//go:embed testdata/call_test.py +var callTest []byte + +var expectedFunctionCalls = []string{"load", "print", "my_other_call", "fun_no_args", "target", "fun_with_no_args", "fun_with_no_args"} + +type CallQuerySuite struct { + suite.Suite +} + +func (suite *CallQuerySuite) newParser() *tree_sitter.Parser { + parser := tree_sitter.NewParser() + + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + suite.Require().NoError(err) + + return parser +} + +func (suite *CallQuerySuite) TestFunctionCallQuery() { + parser := suite.newParser() + pythonTree := parser.Parse(callTest, nil) + + symbols, err := query.QueryCalls(pythonTree, callTest, "") + suite.Require().NoError(err) + + // Extract function call names + callNames := []string{} + for _, s := range symbols { + callNames = append(callNames, s.Name) + } + + suite.Require().NotNil(symbols) + suite.Require().NotEmpty(symbols) + suite.Require().ElementsMatch(expectedFunctionCalls, callNames) +} + +func (suite *CallQuerySuite) TestFunctionCallParameters() { + parser := suite.newParser() + pythonTree := parser.Parse(callTest, nil) + + symbols, err := query.QueryCalls(pythonTree, callTest, "") + suite.Require().NoError(err) + + // Create a map for easier lookup + symbolMap := make(map[string]*symbol.Symbol) + for _, s := range symbols { + symbolMap[s.Name] = s + } + + // Test load function parameters + loadSymbol, ok := symbolMap["load"] + suite.Require().True(ok, "load function should be found") + suite.Require().Len(loadSymbol.Parameters, 2, "load should have 2 parameter") + suite.Require().Equal("0", loadSymbol.Parameters[0].Name, "first parameter should be named '0'") + suite.Require().Equal("\"//folder/to/load\"", loadSymbol.Parameters[0].Value, "load parameter should have correct value") + suite.Require().Equal("1", loadSymbol.Parameters[1].Name, "first parameter should be named '1'") + suite.Require().Equal("\"my_func\"", loadSymbol.Parameters[1].Value, "load parameter should have correct value") + + // Test print function parameters + printSymbol, ok := symbolMap["print"] + suite.Require().True(ok, "print function should be found") + suite.Require().Len(printSymbol.Parameters, 1, "print should have 1 parameter") + suite.Require().Equal("0", printSymbol.Parameters[0].Name, "first parameter should be named '0'") + suite.Require().Equal("\"My Load\"", printSymbol.Parameters[0].Value, "print parameter should have correct value") + + // Test my_other_call function parameters + myOtherCallSymbol, ok := symbolMap["my_other_call"] + suite.Require().True(ok, "my_other_call function should be found") + suite.Require().Len(myOtherCallSymbol.Parameters, 3, "my_other_call should have 3 parameters") + suite.Require().Equal("0", myOtherCallSymbol.Parameters[0].Name, "first parameter should be named '0'") + suite.Require().Equal("arg1", myOtherCallSymbol.Parameters[0].Value, "first parameter should have correct value") + suite.Require().Equal("1", myOtherCallSymbol.Parameters[1].Name, "second parameter should be named '1'") + suite.Require().Equal("kwarg1=\"literal\"", myOtherCallSymbol.Parameters[1].Value, "second parameter should have correct value") + suite.Require().Equal("2", myOtherCallSymbol.Parameters[2].Name, "third parameter should be named '2'") + suite.Require().Equal("kwarg2=12", myOtherCallSymbol.Parameters[2].Value, "third parameter should have correct value") +} + +func (suite *CallQuerySuite) TestExtractFunctions() { + parser := suite.newParser() + pythonTree := parser.Parse(callTest, nil) + + symbols, err := query.QuerySymbols(pythonTree, callTest, "") + suite.Require().NoError(err) + + // Find the my_func symbol + var myFunc *symbol.Symbol + for _, s := range symbols { + if s.Name == "my_func" && s.Kind == symbol.FunctionKind { + myFunc = s + break + } + } + suite.Require().NotNil(myFunc, "my_func should be found") + + // Check parameters + suite.Require().Len(myFunc.Parameters, 3, "my_func should have 3 parameters") + + // param1: str + suite.Require().Equal("param1", myFunc.Parameters[0].Name) + suite.Require().Equal("str", myFunc.Parameters[0].Type) + + // param2: Union[str, int] + suite.Require().Equal("param2", myFunc.Parameters[1].Name) + suite.Require().Equal("Union[str, int]", myFunc.Parameters[1].Type) + + // param3: List[str] with default [] + suite.Require().Equal("param3", myFunc.Parameters[2].Name) + suite.Require().Equal("List[str]", myFunc.Parameters[2].Type) + suite.Require().Equal("[]", myFunc.Parameters[2].Value) +} + +func TestCallQuerySuite(t *testing.T) { + suite.Run(t, &CallQuerySuite{}) +} diff --git a/lsp/runtime/query/query.go b/lsp/runtime/query/query.go new file mode 100644 index 00000000..3653fc5c --- /dev/null +++ b/lsp/runtime/query/query.go @@ -0,0 +1,247 @@ +package query + +import ( + "errors" + "maps" + "slices" + "strings" + + "github.com/hephbuild/heph/lsp/runtime/symbol" + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" +) + +// Examples in https://github.com/tree-sitter/go-tree-sitter/blob/master/query_test.go + +const functionQuery = ` +(function_definition + name: (identifier) @function.name + parameters: (parameters + [ + (identifier) @function.param + (default_parameter ( (identifier) @function.param . (_) @function.param.value )) + (typed_parameter (identifier) @function.param (type (_) @function.param.type)) + (typed_default_parameter ( ((identifier) @function.param) . (type (_) @function.param.type) . ((_) @function.param.value) )) + (list_splat_pattern (identifier) @function.param) + (dictionary_splat_pattern (identifier) @function.param) + ] + )? @function.params + body: (block . + (expression_statement + (string (string_content) )) @function.docstring)?) +` + +const variablesQuery = ` +( + ((comment) @var.comment)? . + (expression_statement + (assignment + left: (identifier) @var.name + right: (_) @var.value + )) +) +` + +var ErrEmptyTreeError = errors.New("empty tree") + +var lang = tree_sitter.NewLanguage(tree_sitter_python.Language()) + +func QuerySymbols(tree *tree_sitter.Tree, text []byte, source string) ([]*symbol.Symbol, error) { + symbols := []*symbol.Symbol{} + + funcSymbols, err := ExtractFunctions(tree, text, source) + if err != nil { + return nil, err + } + symbols = append(symbols, funcSymbols...) + + varSymbols, err := ExtractVariables(tree, text, source) + if err != nil { + return nil, err + } + symbols = append(symbols, varSymbols...) + + return symbols, nil +} + +func ExtractFunctions(tree *tree_sitter.Tree, text []byte, source string) ([]*symbol.Symbol, error) { + if tree.RootNode() == nil { + return nil, ErrEmptyTreeError + } + + query, err := tree_sitter.NewQuery(lang, functionQuery) + if err != nil { + return nil, err + } + + defer query.Close() + + cursor := tree_sitter.NewQueryCursor() + defer cursor.Close() + + matches := cursor.Matches(query, tree.RootNode(), text) + + funs := map[uintptr]*symbol.Symbol{} + + for match := matches.Next(); match != nil; match = matches.Next() { + currSymbol := &symbol.Symbol{Kind: symbol.FunctionKind, Source: source} + var currParam *symbol.Parameter + for _, capture := range match.Captures { + currentNode := &capture.Node + patternName := query.CaptureNames()[capture.Index] + nodeRange := currentNode.Range() + patternValue := currentNode.Utf8Text(text) + + switch patternName { + case "function.name": + // Params query repeats Captures. We use Function Name as id so we dont need to make multiple queries + if ss, ok := funs[currentNode.Id()]; ok { + ss.Parameters = append(ss.Parameters, currSymbol.Parameters...) + currSymbol = ss + } + + // First capture group + currSymbol.Position.RowStart = nodeRange.StartPoint.Row + currSymbol.Position.ColumnStart = nodeRange.StartPoint.Column + + currSymbol.Name = patternValue + currSymbol.Signature = patternValue + "()" // empty params is the default + currSymbol.FullyQualifiedName = patternValue + + funs[currentNode.Id()] = currSymbol + case "function.params": + currSymbol.Signature = currSymbol.Name + patternValue + case "function.param": + currParam = &symbol.Parameter{Name: patternValue} + currSymbol.Parameters = append(currSymbol.Parameters, currParam) + case "function.param.type": + if currParam != nil { + currParam.Type = patternValue + } + case "function.param.value": + if currParam != nil { + currParam.Value = patternValue + } + case "function.docstring": + currSymbol.DocString = sanitizeComment(patternValue) + + // Last capture group + currSymbol.Position.RowEnd = nodeRange.EndPoint.Row + currSymbol.Position.ColumnEnd = nodeRange.EndPoint.Column + } + + } + + parseArgsFromDocstring(currSymbol.DocString, currSymbol.Parameters) + } + + return slices.Collect(maps.Values(funs)), nil +} + +func ExtractVariables(tree *tree_sitter.Tree, text []byte, source string) ([]*symbol.Symbol, error) { + root := tree.RootNode() + if root == nil { + return nil, ErrEmptyTreeError + } + + query, err := tree_sitter.NewQuery(lang, variablesQuery) + if err != nil { + return nil, err + } + + defer query.Close() + + cursor := tree_sitter.NewQueryCursor() + defer cursor.Close() + + vars := []*symbol.Symbol{} + matches := cursor.Matches(query, root, text) + for match := matches.Next(); match != nil; match = matches.Next() { + + currSymbol := &symbol.Symbol{Kind: symbol.VariableKind, Source: source} + for _, capture := range match.Captures { + patternName := query.CaptureNames()[capture.Index] + patternValue := capture.Node.Utf8Text(text) + nodeRange := capture.Node.Range() + + switch patternName { + case "var.name": + // First capture group + currSymbol.Position.RowStart = nodeRange.StartPoint.Row + currSymbol.Position.ColumnStart = nodeRange.StartPoint.Column + + currSymbol.Name = patternValue + currSymbol.Signature = patternValue + currSymbol.FullyQualifiedName = patternValue + case "var.comment": + currSymbol.DocString = sanitizeComment(patternValue) + case "var.value": + currSymbol.Value = patternValue + + // Last capture group + currSymbol.Position.RowEnd = nodeRange.EndPoint.Row + currSymbol.Position.ColumnEnd = nodeRange.EndPoint.Column + } + + } + + vars = append(vars, currSymbol) + } + + return vars, nil +} + +func sanitizeComment(cmmt string) string { + if cmmt, ok := strings.CutPrefix(cmmt, "#"); ok { + return processCommentLines(cmmt) + } + + if cmmt, ok := strings.CutPrefix(cmmt, "\"\"\""); ok { + cmmt, _ = strings.CutSuffix(cmmt, "\"\"\"") + return processCommentLines(cmmt) + } + + return processCommentLines(cmmt) +} + +func processCommentLines(comment string) string { + lines := strings.Split(comment, "\n") + var processedLines []string + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + processedLines = append(processedLines, trimmed) + } + } + + return strings.Join(processedLines, "\n") +} + +func parseArgsFromDocstring(docstring string, params []*symbol.Parameter) { + lines := strings.Split(docstring, "\n") + inArgs := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "Args:" { + inArgs = true + continue + } + if inArgs && strings.Contains(line, ":") { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + namePart := strings.TrimSpace(parts[0]) + desc := strings.TrimSpace(parts[1]) + if idx := strings.Index(namePart, " ("); idx > 0 { + paramName := namePart[:idx] + for _, p := range params { + if p.Name == paramName { + p.DocString = desc + break + } + } + } + } + } + } +} diff --git a/lsp/runtime/query/query_test.go b/lsp/runtime/query/query_test.go new file mode 100644 index 00000000..be2c7886 --- /dev/null +++ b/lsp/runtime/query/query_test.go @@ -0,0 +1,146 @@ +package query_test + +import ( + _ "embed" + "testing" + + "github.com/hephbuild/heph/lsp/runtime/query" + "github.com/hephbuild/heph/lsp/runtime/symbol" + "github.com/stretchr/testify/suite" + + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" +) + +//go:embed testdata/test.py +var pythonTest []byte + +var ( + functionNames = []string{"my_custom_function", "my_other_function", "my_argless_function", "my_documented_function"} + testVariables = []string{"my_custom_variable", "my_new_var", "my_custom_result"} +) + +type QuerySuite struct { + suite.Suite +} + +func (suite *QuerySuite) newParser() *tree_sitter.Parser { + parser := tree_sitter.NewParser() + + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + suite.Require().NoError(err) + + return parser +} + +func (suite *QuerySuite) TestFunctionQuery() { + parser := suite.newParser() + pythonTree := parser.Parse(pythonTest, nil) + + symbols, err := query.ExtractFunctions(pythonTree, pythonTest, "") + suite.Require().NoError(err) + + names := []string{} + for _, s := range symbols { + names = append(names, s.Name) + } + + suite.Require().NotNil(symbols) + suite.Require().NotEmpty(symbols) + suite.Require().ElementsMatch(functionNames, names) +} + +func (suite *QuerySuite) TestFunctionParametersQuery() { + parser := suite.newParser() + pythonTree := parser.Parse(pythonTest, nil) + + symbols, err := query.ExtractFunctions(pythonTree, pythonTest, "") + suite.Require().NoError(err) + + suite.Require().NotNil(symbols) + suite.Require().NotEmpty(symbols) + + // Find the two functions + var customFunc, otherFunc *symbol.Symbol + for _, s := range symbols { + switch s.Name { + case "my_custom_function": + customFunc = s + case "my_other_function": + otherFunc = s + } + } + + suite.Require().NotNil(customFunc, "my_custom_function should be found") + suite.Require().NotNil(otherFunc, "my_other_function should be found") + + // Test my_custom_function parameters + suite.Require().Len(customFunc.Parameters, 2, "my_custom_function should have 2 parameters") + customFuncParamNames := []string{} + for _, param := range customFunc.Parameters { + customFuncParamNames = append(customFuncParamNames, param.Name) + } + suite.Require().Contains(customFuncParamNames, "arg1", "my_custom_function should have arg1 parameter") + suite.Require().Contains(customFuncParamNames, "arg2", "my_custom_function should have arg2 parameter") + + // Test my_other_function parameters + suite.Require().Len(otherFunc.Parameters, 4, "my_other_function should have 3 parameters") + otherFuncParamNames := []string{} + for _, param := range otherFunc.Parameters { + otherFuncParamNames = append(otherFuncParamNames, param.Name) + } + suite.Require().Contains(otherFuncParamNames, "arg1", "my_other_function should have arg1 parameter") + suite.Require().Contains(otherFuncParamNames, "arg2", "my_other_function should have arg2 parameter") + suite.Require().Contains(otherFuncParamNames, "arg3", "my_other_function should have arg3 parameter") + suite.Require().Contains(otherFuncParamNames, "arg4", "my_other_function should have arg4 parameter") +} + +func (suite *QuerySuite) TestVariablesQuery() { + parser := suite.newParser() + pythonTree := parser.Parse(pythonTest, nil) + + symbols, err := query.ExtractVariables(pythonTree, pythonTest, "") + suite.Require().NoError(err) + + names := []string{} + for _, s := range symbols { + names = append(names, s.Name) + } + + suite.Require().NotNil(symbols) + suite.Require().NotEmpty(symbols) + suite.Require().ElementsMatch(testVariables, names) +} + +func (suite *QuerySuite) TestFunctionArgsDoc() { + parser := suite.newParser() + pythonTree := parser.Parse(pythonTest, nil) + + symbols, err := query.ExtractFunctions(pythonTree, pythonTest, "") + suite.Require().NoError(err) + + // Find my_documented_function + var documentedFunc *symbol.Symbol + for _, s := range symbols { + if s.Name == "my_documented_function" { + documentedFunc = s + break + } + } + suite.Require().NotNil(documentedFunc, "my_documented_function should be found") + + suite.Require().Len(documentedFunc.Parameters, 2, "my_documented_function should have 2 parameters") + + suite.Require().Equal("param1", documentedFunc.Parameters[0].Name) + suite.Require().Equal("str", documentedFunc.Parameters[0].Type) + suite.Require().Equal("The first parameter.", documentedFunc.Parameters[0].DocString) + + suite.Require().Equal("param2", documentedFunc.Parameters[1].Name) + suite.Require().Equal("int", documentedFunc.Parameters[1].Type) + suite.Require().Equal("The second parameter. Defaults to 0.", documentedFunc.Parameters[1].DocString) + suite.Require().Equal("0", documentedFunc.Parameters[1].Value) +} + +func TestQuerySuite(t *testing.T) { + suite.Run(t, &QuerySuite{}) +} diff --git a/lsp/runtime/query/symbol.go b/lsp/runtime/query/symbol.go new file mode 100644 index 00000000..8f361911 --- /dev/null +++ b/lsp/runtime/query/symbol.go @@ -0,0 +1,60 @@ +package query + +import tree_sitter "github.com/tree-sitter/go-tree-sitter" + +// ExtractCurrentSymbol from root that is an identifier +func ExtractCurrentSymbol(root *tree_sitter.Node, source []byte, byteOffSet uint) string { + for root != nil && root.Kind() != "identifier" { + root = root.FirstChildForByte(byteOffSet) + } + + if root == nil { + return "" + } + + return root.Utf8Text(source) +} + +// ExtractCurrentStringLiteral finds closes string content +func ExtractCurrentStringLiteral(root *tree_sitter.Node, source []byte, byteOffSet uint) string { + for root != nil && root.Kind() != "string_content" { + root = root.FirstChildForByte(byteOffSet) + } + + if root == nil { + return "" + } + + return root.Utf8Text(source) +} + +// ExtractFunctionNameFromOffset extracts closest current function name whether its a call or definition +func ExtractFunctionNameFromOffset(root *tree_sitter.Node, source []byte, byteOffSet uint) string { + childLookup := "" + for root != nil { + switch root.Kind() { + case "function_definition": + childLookup = "name" + case "call": + childLookup = "function" + } + + if childLookup != "" { + break + } + + root = root.FirstChildForByte(byteOffSet) + } + + if root == nil { + return "" + } + + root = root.ChildByFieldName(childLookup) + + if root == nil { + return "" + } + + return root.Utf8Text(source) +} diff --git a/lsp/runtime/query/symbol_test.go b/lsp/runtime/query/symbol_test.go new file mode 100644 index 00000000..8c818e01 --- /dev/null +++ b/lsp/runtime/query/symbol_test.go @@ -0,0 +1,311 @@ +package query_test + +import ( + _ "embed" + "testing" + + "github.com/hephbuild/heph/lsp/runtime/query" + "github.com/stretchr/testify/suite" + + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" +) + +//go:embed testdata/test_python_symbols.py +var pythonTestFile []byte + +func findByteOffset(source []byte, searchStr string) int { + for i := 0; i < len(source)-len(searchStr); i++ { + if string(source[i:i+len(searchStr)]) == searchStr { + return i + } + } + return -1 +} + +type SymbolTestSuite struct { + suite.Suite + parser *tree_sitter.Parser + tree *tree_sitter.Tree + root *tree_sitter.Node + source []byte +} + +func (s *SymbolTestSuite) SetupSuite() { + s.parser = tree_sitter.NewParser() + err := s.parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) +} + +func (s *SymbolTestSuite) TearDownSuite() { + if s.parser != nil { + s.parser.Close() + } +} + +func (s *SymbolTestSuite) SetupTest() { + s.tree = s.parser.Parse(pythonTestFile, nil) + s.root = s.tree.RootNode() + s.source = pythonTestFile +} + +func (s *SymbolTestSuite) TearDownTest() { + if s.tree != nil { + s.tree.Close() + s.tree = nil + s.root = nil + } +} + +func (s *SymbolTestSuite) TestFindClassName() { + currText := ` + class MyHephClass: + pass + ` + + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse([]byte(currText), nil) + defer tree.Close() + root := tree.RootNode() + + byteOffset := uint(9) + actualSymbol := query.ExtractCurrentSymbol(root, []byte(currText), byteOffset) + + expectedSymbol := "MyHephClass" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestFindVariable() { + currText := `myvar = 12` + + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse([]byte(currText), nil) + defer tree.Close() + root := tree.RootNode() + + byteOffset := uint(2) + actualSymbol := query.ExtractCurrentSymbol(root, []byte(currText), byteOffset) + + expectedSymbol := "myvar" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestFindFunctionNameFromParameter() { + currText := "def my_fun(arg1, arg2):\n\tpass" + + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse([]byte(currText), nil) + defer tree.Close() + root := tree.RootNode() + + // arg1 offset a'r'g1 + byteOffset := uint(13) + actualSymbol := query.ExtractFunctionNameFromOffset(root, []byte(currText), byteOffset) + + expectedSymbol := "my_fun" + s.Equal(expectedSymbol, actualSymbol) + + // arg1 offset a'r'g2 + byteOffset = uint(19) + actualSymbol = query.ExtractFunctionNameFromOffset(root, []byte(currText), byteOffset) + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestFindArgumentNameFromCall() { + currText := "my_fun(arg1=12, arg2=13)" + + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse([]byte(currText), nil) + defer tree.Close() + root := tree.RootNode() + + // 'r' + byteOffset := uint(8) + actualSymbol := query.ExtractCurrentSymbol(root, []byte(currText), byteOffset) + + expectedSymbol := "arg1" + s.Equal(expectedSymbol, actualSymbol) + + // 'a' + byteOffset = uint(16) + actualSymbol = query.ExtractCurrentSymbol(root, []byte(currText), byteOffset) + + expectedSymbol = "arg2" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestFindFunctionNameNoParameter() { + currText := "def my_fun():\n\tpass" + + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse([]byte(currText), nil) + defer tree.Close() + root := tree.RootNode() + + // '(' + byteOffset := uint(11) + actualSymbol := query.ExtractFunctionNameFromOffset(root, []byte(currText), byteOffset) + + expectedSymbol := "my_fun" + s.Equal(expectedSymbol, actualSymbol) + + // ')' + byteOffset = uint(12) + actualSymbol = query.ExtractFunctionNameFromOffset(root, []byte(currText), byteOffset) + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestFindFunctionNameFromCall() { + currText := "my_fun()" + + parser := tree_sitter.NewParser() + err := parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + s.Require().NoError(err) + + tree := parser.Parse([]byte(currText), nil) + defer tree.Close() + root := tree.RootNode() + + // '(' + byteOffset := uint(6) + actualSymbol := query.ExtractFunctionNameFromOffset(root, []byte(currText), byteOffset) + + expectedSymbol := "my_fun" + s.Equal(expectedSymbol, actualSymbol) + + // ')' + byteOffset = uint(7) + actualSymbol = query.ExtractFunctionNameFromOffset(root, []byte(currText), byteOffset) + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_FunctionName() { + offset := findByteOffset(s.source, "hello_world") + s.NotEqual(-1, offset, "Should find 'hello_world' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "hello_world" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_ClassName() { + offset := findByteOffset(s.source, "MyHephClass") + s.NotEqual(-1, offset, "Should find 'MyHephClass' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "MyHephClass" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_MethodName() { + offset := findByteOffset(s.source, "my_method") + s.NotEqual(-1, offset, "Should find 'my_method' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "my_method" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_VariableName() { + offset := findByteOffset(s.source, "my_variable") + s.NotEqual(-1, offset, "Should find 'my_variable' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "my_variable" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_FunctionParameter() { + offset := findByteOffset(s.source, "param1") + s.NotEqual(-1, offset, "Should find 'param1' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "param1" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_AnotherFunction() { + offset := findByteOffset(s.source, "another_function") + s.NotEqual(-1, offset, "Should find 'another_function' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "another_function" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractCurrentSymbol_StaticMethod() { + offset := findByteOffset(s.source, "static_method") + s.NotEqual(-1, offset, "Should find 'static_method' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentSymbol(s.root, s.source, byteOffset) + + expectedSymbol := "static_method" + s.Equal(expectedSymbol, actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractStringLiteral() { + searchStr := "//heph/s" + offset := findByteOffset(s.source, searchStr) + s.NotEqual(-1, offset, "Should find '//heph/string/literal' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentStringLiteral(s.root, s.source, byteOffset) + + s.Equal("//heph/string/literal", actualSymbol) +} + +func (s *SymbolTestSuite) TestExtractStringLiteral_NoString() { + searchStr := "hello_world():" + offset := findByteOffset(s.source, searchStr) + s.NotEqual(-1, offset, "Should find 'hello_world():' in source") + + whitespaceOffset := offset + len(searchStr) + s.True(whitespaceOffset < len(s.source), "Whitespace offset should be within bounds") + + byteOffset := uint(whitespaceOffset) + actualSymbol := query.ExtractCurrentStringLiteral(s.root, s.source, byteOffset) + + s.Equal("", actualSymbol, "Should return empty string when cursor is not on a string literal") +} + +func (s *SymbolTestSuite) TestExtractStringLiteral_OnVariable() { + offset := findByteOffset(s.source, "my_variable") + s.NotEqual(-1, offset, "Should find 'my_variable' in source") + + byteOffset := uint(offset) + actualSymbol := query.ExtractCurrentStringLiteral(s.root, s.source, byteOffset) + + s.Equal("", actualSymbol, "Should return empty string when cursor is on a variable name") +} + +func TestSymbolTestSuite(t *testing.T) { + suite.Run(t, new(SymbolTestSuite)) +} diff --git a/lsp/runtime/query/testdata/call_test.py b/lsp/runtime/query/testdata/call_test.py new file mode 100644 index 00000000..90e5a236 --- /dev/null +++ b/lsp/runtime/query/testdata/call_test.py @@ -0,0 +1,16 @@ +load("//folder/to/load", "my_func") + +print("My Load") + +my_other_call(arg1, kwarg1="literal", kwarg2=12) + +def my_func(param1: str, param2: Union[str, int], param3: List[str] = []): + pass + +fun_no_args() + +a = target(arg1) + +b = fun_with_no_args() + +c = fun_with_no_args() diff --git a/lsp/runtime/query/testdata/test.cst b/lsp/runtime/query/testdata/test.cst new file mode 100644 index 00000000..66070520 --- /dev/null +++ b/lsp/runtime/query/testdata/test.cst @@ -0,0 +1,61 @@ +(module ; [0, 0] - [22, 0] + (import_statement ; [0, 0] - [0, 12] + name: (dotted_name ; [0, 7] - [0, 12] + (identifier))) ; [0, 7] - [0, 12] + (function_definition ; [2, 0] - [6, 36] + name: (identifier) ; [2, 4] - [2, 22] + parameters: (parameters ; [2, 22] - [2, 39] + (typed_parameter ; [2, 23] - [2, 32] + (identifier) ; [2, 23] - [2, 27] + type: (type ; [2, 29] - [2, 32] + (identifier))) ; [2, 29] - [2, 32] + (identifier)) ; [2, 34] - [2, 38] + return_type: (type ; [2, 43] - [2, 46] + (identifier)) ; [2, 43] - [2, 46] + body: (block ; [3, 4] - [6, 36] + (expression_statement ; [3, 4] - [5, 7] + (string ; [3, 4] - [5, 7] + (string_start) ; [3, 4] - [3, 7] + (string_content) ; [3, 7] - [5, 4] + (string_end))) ; [5, 4] - [5, 7] + (return_statement ; [6, 4] - [6, 36] + (binary_operator ; [6, 11] - [6, 36] + left: (binary_operator ; [6, 11] - [6, 29] + left: (string ; [6, 11] - [6, 22] + (string_start) ; [6, 11] - [6, 12] + (string_content) ; [6, 12] - [6, 21] + (string_end)) ; [6, 21] - [6, 22] + right: (identifier)) ; [6, 25] - [6, 29] + right: (identifier))))) ; [6, 32] - [6, 36] + (function_definition ; [9, 0] - [10, 8] + name: (identifier) ; [9, 4] - [9, 21] + parameters: (parameters) ; [9, 21] - [9, 23] + body: (block ; [10, 4] - [10, 8] + (pass_statement))) ; [10, 4] - [10, 8] + (expression_statement ; [13, 0] - [13, 44] + (assignment ; [13, 0] - [13, 44] + left: (identifier) ; [13, 0] - [13, 18] + right: (string ; [13, 21] - [13, 44] + (string_start) ; [13, 21] - [13, 22] + (string_content) ; [13, 22] - [13, 43] + (string_end)))) ; [13, 43] - [13, 44] + (comment) ; [15, 0] - [15, 21] + (expression_statement ; [16, 0] - [16, 15] + (assignment ; [16, 0] - [16, 15] + left: (identifier) ; [16, 0] - [16, 10] + right: (integer))) ; [16, 13] - [16, 15] + (expression_statement ; [18, 0] - [18, 61] + (assignment ; [18, 0] - [18, 61] + left: (identifier) ; [18, 0] - [18, 16] + right: (call ; [18, 19] - [18, 61] + function: (identifier) ; [18, 19] - [18, 37] + arguments: (argument_list ; [18, 37] - [18, 61] + (identifier) ; [18, 38] - [18, 56] + (string ; [18, 58] - [18, 60] + (string_start) ; [18, 58] - [18, 59] + (string_end)))))) ; [18, 59] - [18, 60] + (expression_statement ; [20, 0] - [20, 23] + (call ; [20, 0] - [20, 23] + function: (identifier) ; [20, 0] - [20, 5] + arguments: (argument_list ; [20, 5] - [20, 23] + diff --git a/lsp/runtime/query/testdata/test.py b/lsp/runtime/query/testdata/test.py new file mode 100644 index 00000000..f95567c6 --- /dev/null +++ b/lsp/runtime/query/testdata/test.py @@ -0,0 +1,40 @@ +import types + +def my_custom_function(arg1: str, arg2) -> str: + """ + My custom comment + """ + return "wrapped: " + arg1 + arg2 + + +def my_other_function(arg1, arg2, arg3=12, arg4: str = "abc"): + pass + + +def my_argless_function(): + pass + + +def my_documented_function(param1: str, param2: int = 0): + """ + A function with documented args. + + Args: + param1 (str): The first parameter. + param2 (int, optional): The second parameter. Defaults to 0. + """ + pass + + +my_custom_variable = "custom variable value" + +# My Variable Comment +my_new_var = 12 + +my_custom_result = my_custom_function(my_custom_variable, "") + +print(my_custom_result) + +print("literal_value") + +print(22) diff --git a/lsp/runtime/query/testdata/test_python_symbols.py b/lsp/runtime/query/testdata/test_python_symbols.py new file mode 100644 index 00000000..a169d8c3 --- /dev/null +++ b/lsp/runtime/query/testdata/test_python_symbols.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Test Python file for symbol extraction tests.""" + +def hello_world(): + """A simple function.""" + return "Hello, World!" + +class MyHephClass: + """A test class for Heph.""" + + def __init__(self, name): + self.name = name + + def my_method(self, param1, param2): + """A method with parameters.""" + result = param1 + param2 + return result + + @staticmethod + def static_method(): + """A static method.""" + return "static" + +def another_function(x, y): + """Another function with calculations.""" + z = x * y + my_var = 42 + return z + my_var + +string_literal = "//heph/string/literal" + +# Some variable assignments +my_variable = "test" +another_var = 123 +calculation = another_function(3, 4) + +# Nested function call +result = MyHephClass("test").my_method(1, 2) \ No newline at end of file diff --git a/lsp/runtime/symbol/common.go b/lsp/runtime/symbol/common.go new file mode 100644 index 00000000..4a1a469f --- /dev/null +++ b/lsp/runtime/symbol/common.go @@ -0,0 +1,73 @@ +package symbol + +import "slices" + +func FindSymbol(symbols []*Symbol, sName string) (*Symbol, bool) { + for _, symbol := range symbols { + if symbol.FullyQualifiedName == sName { + return symbol, true + } + + if childS, found := FindSymbol(symbol.Symbols, sName); found { + return childS, true + } + } + + return nil, false +} + +func FindSymbols(symbols []*Symbol, sName string) []*Symbol { + var result []*Symbol + for _, symbol := range symbols { + if symbol.FullyQualifiedName == sName { + result = append(result, symbol) + } + + childResults := FindSymbols(symbol.Symbols, sName) + result = append(result, childResults...) + } + + return result +} + +func FindManySymbol(symbols []*Symbol, sName []string) (*Symbol, bool) { + for _, symbol := range symbols { + if slices.Contains(sName, symbol.FullyQualifiedName) { + return symbol, true + } + + cSym, found := FindManySymbol(symbol.Symbols, sName) + if found { + return cSym, true + } + } + + return nil, false +} + +func FindManySymbols(symbols []*Symbol, sName []string) []*Symbol { + var result []*Symbol + for _, symbol := range symbols { + if slices.Contains(sName, symbol.FullyQualifiedName) { + result = append(result, symbol) + } + + childResults := FindManySymbols(symbol.Symbols, sName) + result = append(result, childResults...) + } + + return result +} + +func FindCalls(symbols []*Symbol, sName string) []*Symbol { + var result []*Symbol + for _, symbol := range symbols { + if (symbol.Kind == FunctionCallKind || symbol.Kind == TargetCallKind) && symbol.FullyQualifiedName == sName { + result = append(result, symbol) + } + + childResults := FindCalls(symbol.Symbols, sName) + result = append(result, childResults...) + } + return result +} diff --git a/lsp/runtime/symbol/symbol.go b/lsp/runtime/symbol/symbol.go new file mode 100644 index 00000000..b506cc29 --- /dev/null +++ b/lsp/runtime/symbol/symbol.go @@ -0,0 +1,61 @@ +package symbol + +type SymbolKind int + +// Kind types +const ( + ClassKind SymbolKind = iota + FunctionKind + VariableKind +) + +// Calls types +const ( + FunctionCallKind = iota + 4 + TargetCallKind +) + +type Position struct { + RowStart uint + ColumnStart uint + RowEnd uint + ColumnEnd uint +} + +type rawPosition struct { + ByteStart uint + ByteEnd uint +} + +type Parameter struct { + Name string + Type string + Value string + DocString string +} + +type Symbol struct { + Name string + Source string + + FullyQualifiedName string + + Kind SymbolKind + Signature string + + Parameters []*Parameter + + // Value is the current literal value for a variable + Value string + DocString string + + Position Position + + SignaturePosition rawPosition + + Symbols []*Symbol +} + +func (s *Symbol) Is(kind SymbolKind) bool { + return s.Kind == kind +} diff --git a/lsp/server.go b/lsp/server.go new file mode 100644 index 00000000..c95e88a5 --- /dev/null +++ b/lsp/server.go @@ -0,0 +1,176 @@ +package lsp + +import ( + "errors" + "sync" + + "github.com/hephbuild/heph/hroot" + "github.com/hephbuild/heph/lsp/runtime" + "github.com/hephbuild/heph/vfssimple" + + "github.com/hephbuild/heph/lsp/capabilities/lang" + "github.com/hephbuild/heph/lsp/capabilities/lifecycle" + docsync "github.com/hephbuild/heph/lsp/capabilities/sync" + + tree_sitter "github.com/tree-sitter/go-tree-sitter" + tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" + + "github.com/tliron/commonlog" + _ "github.com/tliron/commonlog/simple" + + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" + "github.com/tliron/glsp/server" +) + +var ErrIsClosed = errors.New("server is closed") + +type LSPServer interface { + // Serve blocks until the server stops serving. Errors encountered during this process are returned. + // Serve will be called only once for the lifetime of an LSPServer + Serve() error + Close() error +} + +type hephLSP struct { + protocolHandler *protocol.Handler + server *server.Server + parser *tree_sitter.Parser + + isClosed bool + + // Force non-copy + _ [0]sync.Mutex +} + +func NewHephServer(root *hroot.State) (LSPServer, error) { + return newHephLSP(root, false) +} + +func (h *hephLSP) Serve() error { + if h.isClosed { + return ErrIsClosed + } + + return h.server.RunStdio() +} + +func (h *hephLSP) Close() error { + h.parser.Close() + h.server.GetStdio().Close() //nolint + h.isClosed = true + + return nil +} + +func newHephLSP(root *hroot.State, debug bool) (*hephLSP, error) { + err := configureLogs(root, debug) + if err != nil { + return nil, err + } + + lsp := &hephLSP{} + + parser := tree_sitter.NewParser() + err = parser.SetLanguage(tree_sitter.NewLanguage(tree_sitter_python.Language())) + if err != nil { + return nil, err + } + + manager, err := runtime.NewManager(parser) + if err != nil { + return nil, err + } + + handler := &protocol.Handler{ + // Lifecycle + Initialize: lsp.wrapInitialize(manager), + Initialized: lsp.wrapInitialized(), + Shutdown: lsp.wrapShutdown(), + SetTrace: lsp.wrapSetTrace(), + + // Sync + TextDocumentDidOpen: docsync.TextDocumentDidOpenWrapper(manager), + TextDocumentDidChange: docsync.TextDocumentDidChangeFuncWrapper(manager), + + // Lang features + TextDocumentCompletion: lang.TextDocumentCompletionFuncWrapper(manager), + TextDocumentHover: lang.TextDocumentHoverFuncWrapper(manager), + + // Can be implemented Implement code lens to copy addr or give in virtual text a full path of target etc + // TextDocumentCodeLens: TextDocumentCodeLensFunc + TextDocumentReferences: lang.TextDocumentReferencesFuncWrapper(manager), + TextDocumentDeclaration: lang.TextDocumentDeclarationFuncWrapper(manager), + TextDocumentDefinition: lang.TextDocumentDefinitionFuncWrapper(manager), + + WorkspaceDidRenameFiles: docsync.WorkspaceDidRenameFilesFunc(manager), + WorkspaceDidDeleteFiles: docsync.WorkspaceDidDeleteFilesFunc(manager), + } + server := server.NewServer(handler, runtime.HephLanguage, debug) + + lsp.protocolHandler = handler + lsp.server = server + lsp.parser = parser + + return lsp, nil +} + +func configureLogs(root *hroot.State, debug bool) error { + verbosity := 0 + if debug { + verbosity = 2 + } + + logpath := root.Home.Join(".lsplogs") + fullpath := logpath.Abs() + dst, err := vfssimple.NewFile("file://" + fullpath) + if err != nil { + return err + } + defer dst.Close() + + // tliron/glsp forces us to use this weird logger + commonlog.Configure(verbosity, &fullpath) + + return nil +} + +func (h *hephLSP) wrapInitialize(manager *runtime.Manager) protocol.InitializeFunc { + return func(context *glsp.Context, params *protocol.InitializeParams) (any, error) { + // Call lifecycle callback + err := lifecycle.InitializeCallback(manager, context, params) + if err != nil { + return nil, err + } + + capabilities := h.protocolHandler.CreateServerCapabilities() + + return protocol.InitializeResult{ + Capabilities: capabilities, + ServerInfo: &protocol.InitializeResultServerInfo{ + Name: runtime.HephLanguage, + Version: &runtime.Version, + }, + }, nil + } +} + +func (h *hephLSP) wrapInitialized() protocol.InitializedFunc { + return func(context *glsp.Context, params *protocol.InitializedParams) error { + return nil + } +} + +func (h *hephLSP) wrapShutdown() protocol.ShutdownFunc { + return func(context *glsp.Context) error { + protocol.SetTraceValue(protocol.TraceValueOff) + return nil + } +} + +func (h *hephLSP) wrapSetTrace() protocol.SetTraceFunc { + return func(context *glsp.Context, params *protocol.SetTraceParams) error { + protocol.SetTraceValue(params.Value) + return nil + } +}