From 28316544f396f191007b3f22993b6a7adcb232be Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 9 May 2024 15:08:51 +0100 Subject: [PATCH 1/8] lsp/completions: Add completions manager & example Signed-off-by: Charlie Egan --- go.mod | 46 ++-- go.sum | 104 ++++---- internal/lsp/cache.go | 240 ----------------- internal/lsp/cache/cache.go | 248 ++++++++++++++++++ internal/lsp/completions/manager.go | 54 ++++ internal/lsp/completions/manager_test.go | 45 ++++ internal/lsp/completions/providers/package.go | 67 +++++ .../lsp/completions/providers/package_test.go | 112 ++++++++ .../lsp/completions/providers/packagename.go | 70 +++++ .../completions/providers/packagename_test.go | 114 ++++++++ internal/lsp/hover.go | 16 +- internal/lsp/lint.go | 7 +- internal/lsp/server.go | 51 +++- internal/lsp/server_test.go | 7 +- internal/lsp/types/internal.go | 10 + internal/lsp/types/types.go | 42 +++ 16 files changed, 885 insertions(+), 348 deletions(-) create mode 100644 internal/lsp/cache/cache.go create mode 100644 internal/lsp/completions/manager.go create mode 100644 internal/lsp/completions/manager_test.go create mode 100644 internal/lsp/completions/providers/package.go create mode 100644 internal/lsp/completions/providers/package_test.go create mode 100644 internal/lsp/completions/providers/packagename.go create mode 100644 internal/lsp/completions/providers/packagename_test.go create mode 100644 internal/lsp/types/internal.go diff --git a/go.mod b/go.mod index 39d2ce6e..d1689981 100644 --- a/go.mod +++ b/go.mod @@ -21,59 +21,43 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require ( - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/gdamore/encoding v1.0.0 // indirect - github.com/gdamore/tcell v1.4.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/stretchr/testify v1.9.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.19.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect -) - require ( github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/fgprof v0.9.3 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell v1.1.4 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect - github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/lucasb-eyer/go-colorful v1.0.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.10 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.51.1 // indirect - github.com/prometheus/procfs v0.13.0 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - // wasip1 support isn't in a tagged version yet, remove for v1.9.4 - github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/rivo/uniseg v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index ca74f224..4afe8bd1 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -32,8 +32,8 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -48,9 +48,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.1.4 h1:6Bubmk3vZvnL9umQ9qTV2kwNQnjaZ4HLAbxR+xR3ATg= github.com/gdamore/tcell v1.1.4/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= -github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= -github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -78,22 +77,21 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= -github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -102,20 +100,17 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -131,28 +126,25 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw= -github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 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.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= -github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -164,8 +156,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -179,25 +171,25 @@ github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Y github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= @@ -217,13 +209,13 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa h1:Jt1XW5PaLXF1/ePZrznsh/aAUvI7Adfc3LY1dAKlzRs= -google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:K4kfzHtI0kqWA79gecJarFtDn/Mls+GxQcg3Zox91Ac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= diff --git a/internal/lsp/cache.go b/internal/lsp/cache.go index 3897a58b..6e7e47a1 100644 --- a/internal/lsp/cache.go +++ b/internal/lsp/cache.go @@ -1,241 +1 @@ package lsp - -import ( - "fmt" - "os" - "sync" - - "github.com/open-policy-agent/opa/ast" - - "github.com/styrainc/regal/internal/lsp/types" -) - -// Cache is used to store: current file contents (which includes unsaved changes), the latest parsed modules, and -// diagnostics for each file (including diagnostics gathered from linting files alongside other files). -type Cache struct { - // fileContents is a map of file URI to raw file contents received from the client - fileContents map[string]string - fileContentsMu sync.Mutex - - // modules is a map of file URI to parsed AST modules from the latest file contents value - modules map[string]*ast.Module - moduleMu sync.Mutex - - // diagnosticsFile is a map of file URI to diagnostics for that file - diagnosticsFile map[string][]types.Diagnostic - diagnosticsFileMu sync.Mutex - - // diagnosticsAggregate is a map of file URI to aggregate diagnostics for that file - diagnosticsAggregate map[string][]types.Diagnostic - diagnosticsAggregateMu sync.Mutex - - // diagnosticsParseErrors is a map of file URI to parse errors for that file - diagnosticsParseErrors map[string][]types.Diagnostic - diagnosticsParseMu sync.Mutex - - builtinPositionsFile map[string]map[uint][]BuiltinPosition - builtinPositionsMu sync.Mutex -} - -func NewCache() *Cache { - return &Cache{ - fileContents: make(map[string]string), - modules: make(map[string]*ast.Module), - - diagnosticsFile: make(map[string][]types.Diagnostic), - diagnosticsAggregate: make(map[string][]types.Diagnostic), - diagnosticsParseErrors: make(map[string][]types.Diagnostic), - - builtinPositionsFile: make(map[string]map[uint][]BuiltinPosition), - } -} - -func (c *Cache) GetAllDiagnosticsForURI(uri string) []types.Diagnostic { - parseDiags, ok := c.GetParseErrors(uri) - if ok && len(parseDiags) > 0 { - return parseDiags - } - - allDiags := make([]types.Diagnostic, 0) - - aggDiags, ok := c.GetAggregateDiagnostics(uri) - if ok { - allDiags = append(allDiags, aggDiags...) - } - - fileDiags, ok := c.GetFileDiagnostics(uri) - if ok { - allDiags = append(allDiags, fileDiags...) - } - - return allDiags -} - -func (c *Cache) GetAllFiles() map[string]string { - c.fileContentsMu.Lock() - defer c.fileContentsMu.Unlock() - - return c.fileContents -} - -func (c *Cache) GetFileContents(uri string) (string, bool) { - c.fileContentsMu.Lock() - defer c.fileContentsMu.Unlock() - - val, ok := c.fileContents[uri] - - return val, ok -} - -func (c *Cache) SetFileContents(uri string, content string) { - c.fileContentsMu.Lock() - defer c.fileContentsMu.Unlock() - - c.fileContents[uri] = content -} - -func (c *Cache) GetAllModules() map[string]*ast.Module { - c.moduleMu.Lock() - defer c.moduleMu.Unlock() - - return c.modules -} - -func (c *Cache) GetModule(uri string) (*ast.Module, bool) { - c.moduleMu.Lock() - defer c.moduleMu.Unlock() - - val, ok := c.modules[uri] - - return val, ok -} - -func (c *Cache) SetModule(uri string, module *ast.Module) { - c.moduleMu.Lock() - defer c.moduleMu.Unlock() - - c.modules[uri] = module -} - -func (c *Cache) GetFileDiagnostics(uri string) ([]types.Diagnostic, bool) { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - val, ok := c.diagnosticsFile[uri] - - return val, ok -} - -func (c *Cache) SetFileDiagnostics(uri string, diags []types.Diagnostic) { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - c.diagnosticsFile[uri] = diags -} - -func (c *Cache) ClearFileDiagnostics() { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - c.diagnosticsFile = make(map[string][]types.Diagnostic) -} - -func (c *Cache) GetAggregateDiagnostics(uri string) ([]types.Diagnostic, bool) { - c.diagnosticsAggregateMu.Lock() - defer c.diagnosticsAggregateMu.Unlock() - - val, ok := c.diagnosticsAggregate[uri] - - return val, ok -} - -func (c *Cache) SetAggregateDiagnostics(uri string, diags []types.Diagnostic) { - c.diagnosticsAggregateMu.Lock() - defer c.diagnosticsAggregateMu.Unlock() - - c.diagnosticsAggregate[uri] = diags -} - -func (c *Cache) ClearAggregateDiagnostics() { - c.diagnosticsAggregateMu.Lock() - defer c.diagnosticsAggregateMu.Unlock() - - c.diagnosticsAggregate = make(map[string][]types.Diagnostic) -} - -func (c *Cache) GetParseErrors(uri string) ([]types.Diagnostic, bool) { - c.diagnosticsParseMu.Lock() - defer c.diagnosticsParseMu.Unlock() - - val, ok := c.diagnosticsParseErrors[uri] - - return val, ok -} - -func (c *Cache) SetParseErrors(uri string, diags []types.Diagnostic) { - c.diagnosticsParseMu.Lock() - defer c.diagnosticsParseMu.Unlock() - - c.diagnosticsParseErrors[uri] = diags -} - -func (c *Cache) GetBuiltinPositions(uri string) (map[uint][]BuiltinPosition, bool) { - c.builtinPositionsMu.Lock() - defer c.builtinPositionsMu.Unlock() - - val, ok := c.builtinPositionsFile[uri] - - return val, ok -} - -func (c *Cache) SetBuiltinPositions(uri string, positions map[uint][]BuiltinPosition) { - c.builtinPositionsMu.Lock() - defer c.builtinPositionsMu.Unlock() - - c.builtinPositionsFile[uri] = positions -} - -// Delete removes all cached data for a given URI. -func (c *Cache) Delete(uri string) { - c.fileContentsMu.Lock() - delete(c.fileContents, uri) - c.fileContentsMu.Unlock() - - c.moduleMu.Lock() - delete(c.modules, uri) - c.moduleMu.Unlock() - - c.diagnosticsFileMu.Lock() - delete(c.diagnosticsFile, uri) - c.diagnosticsFileMu.Unlock() - - c.diagnosticsAggregateMu.Lock() - delete(c.diagnosticsAggregate, uri) - c.diagnosticsAggregateMu.Unlock() - - c.diagnosticsParseMu.Lock() - delete(c.diagnosticsParseErrors, uri) - c.diagnosticsParseMu.Unlock() - - c.builtinPositionsMu.Lock() - delete(c.builtinPositionsFile, uri) - c.builtinPositionsMu.Unlock() -} - -func updateCacheForURIFromDisk(cache *Cache, uri, path string) (string, error) { - content, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("failed to read file: %w", err) - } - - currentContent := string(content) - - cachedContent, ok := cache.GetFileContents(uri) - if ok && cachedContent == currentContent { - return cachedContent, nil - } - - cache.SetFileContents(uri, currentContent) - - return currentContent, nil -} diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go new file mode 100644 index 00000000..7b56045e --- /dev/null +++ b/internal/lsp/cache/cache.go @@ -0,0 +1,248 @@ +package cache + +import ( + "fmt" + "os" + "sync" + + "github.com/open-policy-agent/opa/ast" + + "github.com/styrainc/regal/internal/lsp/types" +) + +// Cache is used to store: current file contents (which includes unsaved changes), the latest parsed modules, and +// diagnostics for each file (including diagnostics gathered from linting files alongside other files). +type Cache struct { + // fileContents is a map of file URI to raw file contents received from the client + fileContents map[string]string + fileContentsMu sync.Mutex + + // modules is a map of file URI to parsed AST modules from the latest file contents value + modules map[string]*ast.Module + moduleMu sync.Mutex + + // diagnosticsFile is a map of file URI to diagnostics for that file + diagnosticsFile map[string][]types.Diagnostic + diagnosticsFileMu sync.Mutex + + // diagnosticsAggregate is a map of file URI to aggregate diagnostics for that file + diagnosticsAggregate map[string][]types.Diagnostic + diagnosticsAggregateMu sync.Mutex + + // diagnosticsParseErrors is a map of file URI to parse errors for that file + diagnosticsParseErrors map[string][]types.Diagnostic + diagnosticsParseMu sync.Mutex + + builtinPositionsFile map[string]map[uint][]types.BuiltinPosition + builtinPositionsMu sync.Mutex +} + +func NewCache() *Cache { + return &Cache{ + fileContents: make(map[string]string), + modules: make(map[string]*ast.Module), + + diagnosticsFile: make(map[string][]types.Diagnostic), + diagnosticsAggregate: make(map[string][]types.Diagnostic), + diagnosticsParseErrors: make(map[string][]types.Diagnostic), + + builtinPositionsFile: make(map[string]map[uint][]types.BuiltinPosition), + } +} + +func (c *Cache) GetAllDiagnosticsForURI(uri string) []types.Diagnostic { + parseDiags, ok := c.GetParseErrors(uri) + if ok && len(parseDiags) > 0 { + return parseDiags + } + + allDiags := make([]types.Diagnostic, 0) + + aggDiags, ok := c.GetAggregateDiagnostics(uri) + if ok { + allDiags = append(allDiags, aggDiags...) + } + + fileDiags, ok := c.GetFileDiagnostics(uri) + if ok { + allDiags = append(allDiags, fileDiags...) + } + + return allDiags +} + +func (c *Cache) GetAllFiles() map[string]string { + c.fileContentsMu.Lock() + defer c.fileContentsMu.Unlock() + + return c.fileContents +} + +func (c *Cache) GetFileContents(uri string) (string, bool) { + c.fileContentsMu.Lock() + defer c.fileContentsMu.Unlock() + + val, ok := c.fileContents[uri] + + return val, ok +} + +func (c *Cache) SetFileContents(uri string, content string) { + c.fileContentsMu.Lock() + defer c.fileContentsMu.Unlock() + + c.fileContents[uri] = content +} + +func (c *Cache) GetAllModules() map[string]*ast.Module { + c.moduleMu.Lock() + defer c.moduleMu.Unlock() + + return c.modules +} + +func (c *Cache) GetModule(uri string) (*ast.Module, bool) { + c.moduleMu.Lock() + defer c.moduleMu.Unlock() + + val, ok := c.modules[uri] + + return val, ok +} + +func (c *Cache) SetModule(uri string, module *ast.Module) { + c.moduleMu.Lock() + defer c.moduleMu.Unlock() + + c.modules[uri] = module +} + +func (c *Cache) GetFileDiagnostics(uri string) ([]types.Diagnostic, bool) { + c.diagnosticsFileMu.Lock() + defer c.diagnosticsFileMu.Unlock() + + val, ok := c.diagnosticsFile[uri] + + return val, ok +} + +func (c *Cache) SetFileDiagnostics(uri string, diags []types.Diagnostic) { + c.diagnosticsFileMu.Lock() + defer c.diagnosticsFileMu.Unlock() + + c.diagnosticsFile[uri] = diags +} + +func (c *Cache) ClearFileDiagnostics() { + c.diagnosticsFileMu.Lock() + defer c.diagnosticsFileMu.Unlock() + + c.diagnosticsFile = make(map[string][]types.Diagnostic) +} + +func (c *Cache) GetAggregateDiagnostics(uri string) ([]types.Diagnostic, bool) { + c.diagnosticsAggregateMu.Lock() + defer c.diagnosticsAggregateMu.Unlock() + + val, ok := c.diagnosticsAggregate[uri] + + return val, ok +} + +func (c *Cache) SetAggregateDiagnostics(uri string, diags []types.Diagnostic) { + c.diagnosticsAggregateMu.Lock() + defer c.diagnosticsAggregateMu.Unlock() + + c.diagnosticsAggregate[uri] = diags +} + +func (c *Cache) ClearAggregateDiagnostics() { + c.diagnosticsAggregateMu.Lock() + defer c.diagnosticsAggregateMu.Unlock() + + c.diagnosticsAggregate = make(map[string][]types.Diagnostic) +} + +func (c *Cache) GetParseErrors(uri string) ([]types.Diagnostic, bool) { + c.diagnosticsParseMu.Lock() + defer c.diagnosticsParseMu.Unlock() + + val, ok := c.diagnosticsParseErrors[uri] + + return val, ok +} + +func (c *Cache) SetParseErrors(uri string, diags []types.Diagnostic) { + c.diagnosticsParseMu.Lock() + defer c.diagnosticsParseMu.Unlock() + + c.diagnosticsParseErrors[uri] = diags +} + +func (c *Cache) GetBuiltinPositions(uri string) (map[uint][]types.BuiltinPosition, bool) { + c.builtinPositionsMu.Lock() + defer c.builtinPositionsMu.Unlock() + + val, ok := c.builtinPositionsFile[uri] + + return val, ok +} + +func (c *Cache) SetBuiltinPositions(uri string, positions map[uint][]types.BuiltinPosition) { + c.builtinPositionsMu.Lock() + defer c.builtinPositionsMu.Unlock() + + c.builtinPositionsFile[uri] = positions +} + +func (c *Cache) GetAllBuiltInPositions() map[string]map[uint][]types.BuiltinPosition { + c.builtinPositionsMu.Lock() + defer c.builtinPositionsMu.Unlock() + + return c.builtinPositionsFile +} + +// Delete removes all cached data for a given URI. +func (c *Cache) Delete(uri string) { + c.fileContentsMu.Lock() + delete(c.fileContents, uri) + c.fileContentsMu.Unlock() + + c.moduleMu.Lock() + delete(c.modules, uri) + c.moduleMu.Unlock() + + c.diagnosticsFileMu.Lock() + delete(c.diagnosticsFile, uri) + c.diagnosticsFileMu.Unlock() + + c.diagnosticsAggregateMu.Lock() + delete(c.diagnosticsAggregate, uri) + c.diagnosticsAggregateMu.Unlock() + + c.diagnosticsParseMu.Lock() + delete(c.diagnosticsParseErrors, uri) + c.diagnosticsParseMu.Unlock() + + c.builtinPositionsMu.Lock() + delete(c.builtinPositionsFile, uri) + c.builtinPositionsMu.Unlock() +} + +func UpdateCacheForURIFromDisk(cache *Cache, uri, path string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } + + currentContent := string(content) + + cachedContent, ok := cache.GetFileContents(uri) + if ok && cachedContent == currentContent { + return cachedContent, nil + } + + cache.SetFileContents(uri, currentContent) + + return currentContent, nil +} diff --git a/internal/lsp/completions/manager.go b/internal/lsp/completions/manager.go new file mode 100644 index 00000000..06c38cc8 --- /dev/null +++ b/internal/lsp/completions/manager.go @@ -0,0 +1,54 @@ +package completions + +import ( + "fmt" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/completions/providers" + "github.com/styrainc/regal/internal/lsp/types" +) + +type Manager struct { + c *cache.Cache + opts *ManagerOptions + providers []Provider +} + +type ManagerOptions struct{} + +type Provider interface { + Run(*cache.Cache, types.CompletionParams) ([]types.CompletionItem, error) +} + +func NewManager(c *cache.Cache, opts *ManagerOptions) *Manager { + return &Manager{c: c, opts: opts} +} + +func NewDefaultManager(c *cache.Cache) *Manager { + m := NewManager(c, &ManagerOptions{}) + + m.RegisterProvider(&providers.Package{}) + m.RegisterProvider(&providers.PackageName{}) + + return m +} + +func (m *Manager) Run(params types.CompletionParams) ([]types.CompletionItem, error) { + var completions []types.CompletionItem + + for _, provider := range m.providers { + providerCompletions, err := provider.Run(m.c, params) + if err != nil { + return nil, fmt.Errorf("error running completion provider: %w", err) + } + if len(providerCompletions) > 0 { + completions = append(completions, providerCompletions...) + } + } + + return completions, nil +} + +func (m *Manager) RegisterProvider(provider Provider) { + m.providers = append(m.providers, provider) +} diff --git a/internal/lsp/completions/manager_test.go b/internal/lsp/completions/manager_test.go new file mode 100644 index 00000000..7ff6d379 --- /dev/null +++ b/internal/lsp/completions/manager_test.go @@ -0,0 +1,45 @@ +package completions + +import ( + "testing" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/completions/providers" + "github.com/styrainc/regal/internal/lsp/types" +) + +func TestManager(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := "" + + c.SetFileContents(fileURI, fileContents) + + mgr := NewManager(c, &ManagerOptions{}) + mgr.RegisterProvider(&providers.Package{}) + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 0, + Character: 1, + }, + } + + completions, err := mgr.Run(completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 1 { + t.Fatalf("Expected exactly one completion, got: %v", completions) + } + + comp := completions[0] + if comp.Label != "package" { + t.Fatalf("Expected label to be 'package', got: %v", comp.Label) + } +} diff --git a/internal/lsp/completions/providers/package.go b/internal/lsp/completions/providers/package.go new file mode 100644 index 00000000..edd273ba --- /dev/null +++ b/internal/lsp/completions/providers/package.go @@ -0,0 +1,67 @@ +package providers + +import ( + "fmt" + "os" + "strings" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/types" +) + +// Package will return completions for the package keyword when starting a new file. +type Package struct{} + +func (p *Package) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { + + fileURI := params.TextDocument.URI + fileContents, ok := c.GetFileContents(fileURI) + if !ok { + // if the file contents is missing then we can't provide completions + return nil, nil + } + + lines := strings.Split(fileContents, "\n") + if len(lines) < 1 { + return nil, nil + } + + for i, line := range lines { + if i < int(params.Position.Line) && strings.HasPrefix(line, "package ") { + // if there is already a package statement in the file then we don't provide any more completions + return nil, nil + } + } + + // if we can't confirm that the user has package statement on the line then we don't provide completions + if len(lines)+1 < int(params.Position.Line) { + fmt.Fprintln(os.Stderr, "no package statement") + return nil, nil + } + + // if not on the first line, the user must type p before we provide completions + if params.Position.Line != 0 && !strings.HasPrefix(lines[params.Position.Line], "p") { + return nil, nil + } + + return []types.CompletionItem{ + { + Label: "package", + Kind: 14, // 14 is the kind for keyword + Detail: "package ", + TextEdit: &types.TextEdit{ + Range: types.Range{ + Start: types.Position{ + Line: params.Position.Line, + Character: 0, + }, + End: types.Position{ + Line: params.Position.Line, + Character: params.Position.Character, + }, + }, + NewText: "package ", + }, + }, + }, nil +} diff --git a/internal/lsp/completions/providers/package_test.go b/internal/lsp/completions/providers/package_test.go new file mode 100644 index 00000000..657962e6 --- /dev/null +++ b/internal/lsp/completions/providers/package_test.go @@ -0,0 +1,112 @@ +package providers + +import ( + "testing" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/types" +) + +func TestPackage(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := "\n" + + c.SetFileContents(fileURI, fileContents) + + p := &Package{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 0, + Character: 0, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 1 { + t.Fatalf("Expected exactly one completion, got: %v", completions) + } + + comp := completions[0] + if comp.Label != "package" { + t.Fatalf("Expected label to be 'package', got: %v", comp.Label) + } +} + +func TestPackageAfterComment(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := ` +# this is a comment before the package statement +p + +` + + c.SetFileContents(fileURI, fileContents) + + p := &Package{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 2, + Character: 1, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 1 { + t.Fatalf("Expected exactly one completion, got: %v", completions) + } + + comp := completions[0] + if comp.Label != "package" { + t.Fatalf("Expected label to be 'package', got: %v", comp.Label) + } +} + +func TestPackageNotLaterLines(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := "package foo\n\n" + + c.SetFileContents(fileURI, fileContents) + + p := &Package{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 1, + Character: 0, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 0 { + t.Fatalf("Expected no completions, got: %v", completions) + } +} diff --git a/internal/lsp/completions/providers/packagename.go b/internal/lsp/completions/providers/packagename.go new file mode 100644 index 00000000..3f4f0ca5 --- /dev/null +++ b/internal/lsp/completions/providers/packagename.go @@ -0,0 +1,70 @@ +package providers + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/clients" + "github.com/styrainc/regal/internal/lsp/types" + "github.com/styrainc/regal/internal/lsp/uri" +) + +// PackageName will return completions for the package name when starting a new file based on the file's URI. +type PackageName struct{} + +func (p *PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { + + fileURI := params.TextDocument.URI + fileContents, ok := c.GetFileContents(fileURI) + if !ok { + // if the file contents is missing then we can't provide completions + return nil, nil + } + + lines := strings.Split(fileContents, "\n") + if len(lines) < 1 { + return nil, nil + } + + for i, line := range lines { + if i < int(params.Position.Line) && strings.HasPrefix(line, "package ") { + // if there is already a package statement in the file then we don't provide any more completions + return nil, nil + } + } + + // if we can't confirm that the user has package statement on the line then we don't provide completions + if len(lines)+1 < int(params.Position.Line) { + return nil, nil + } + + path := uri.ToPath(clients.IdentifierGeneric, fileURI) + dir := filepath.Base(filepath.Dir(path)) + + if !strings.HasPrefix(lines[params.Position.Line], "p") { + return nil, nil + } + + return []types.CompletionItem{ + { + Label: fmt.Sprintf("package %s", dir), + Detail: "suggested package name based on directory", + Kind: 19, // 19 is the kind for a folder + TextEdit: &types.TextEdit{ + Range: types.Range{ + Start: types.Position{ + Line: params.Position.Line, + Character: 0, + }, + End: types.Position{ + Line: params.Position.Line, + Character: params.Position.Character, + }, + }, + NewText: fmt.Sprintf("package %s\n\n", dir), + }, + }, + }, nil +} diff --git a/internal/lsp/completions/providers/packagename_test.go b/internal/lsp/completions/providers/packagename_test.go new file mode 100644 index 00000000..b52eeecf --- /dev/null +++ b/internal/lsp/completions/providers/packagename_test.go @@ -0,0 +1,114 @@ +package providers + +import ( + "testing" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/types" +) + +func TestPackageName(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := "package " + + c.SetFileContents(fileURI, fileContents) + + p := &PackageName{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 0, + Character: 9, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 1 { + t.Fatalf("Expected exactly one completion, got: %v", completions) + } + + comp := completions[0] + if comp.Label != "package bar" { + t.Fatalf("Expected label to be 'bar', got: %v", comp.Label) + } +} + +func TestPackageNameWithPackageComment(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///bar/foo/file.rego" + fileContents := ` +# this is a comment before the package statement +# at the start of a file + +package ` + + c.SetFileContents(fileURI, fileContents) + + p := &PackageName{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 4, + Character: 9, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 1 { + t.Fatalf("Expected exactly one completion, got: %v", completions) + } + + comp := completions[0] + if comp.Label != "package foo" { + t.Fatalf("Expected label to be 'foo', got: %v", comp.Label) + } +} + +func TestPackageNameWithErroneousPackageStatements(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := `package foo + +package ` + + c.SetFileContents(fileURI, fileContents) + + p := &PackageName{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 4, + Character: 9, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 0 { + t.Fatalf("Expected no completions, got: %v", completions) + } +} diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index ff3cd8e7..9df05758 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -8,14 +8,10 @@ import ( "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/types" -) -type BuiltinPosition struct { - Builtin *ast.Builtin - Line uint - Start uint - End uint -} + "github.com/styrainc/regal/internal/lsp/cache" + types2 "github.com/styrainc/regal/internal/lsp/types" +) var builtins = builtinMap() //nolint:gochecknoglobals @@ -152,18 +148,18 @@ func createHoverContent(builtin *ast.Builtin) string { return result } -func updateBuiltinPositions(cache *Cache, uri string) error { +func updateBuiltinPositions(cache *cache.Cache, uri string) error { module, ok := cache.GetModule(uri) if !ok { return fmt.Errorf("failed to update builtin positions: no parsed module for uri %q", uri) } - builtinsOnLine := map[uint][]BuiltinPosition{} + builtinsOnLine := map[uint][]types2.BuiltinPosition{} for _, call := range AllBuiltinCalls(module) { line := uint(call.Location.Row) - builtinsOnLine[line] = append(builtinsOnLine[line], BuiltinPosition{ + builtinsOnLine[line] = append(builtinsOnLine[line], types2.BuiltinPosition{ Builtin: call.Builtin, Line: line, Start: uint(call.Location.Col), diff --git a/internal/lsp/lint.go b/internal/lsp/lint.go index bfde20a6..71d105b4 100644 --- a/internal/lsp/lint.go +++ b/internal/lsp/lint.go @@ -9,6 +9,7 @@ import ( "github.com/open-policy-agent/opa/ast" + "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/types" rparse "github.com/styrainc/regal/internal/parse" "github.com/styrainc/regal/pkg/config" @@ -19,7 +20,7 @@ import ( // updateParse updates the module cache with the latest parse result for a given URI, // if the module cannot be parsed, the parse errors are saved as diagnostics for the // URI instead. -func updateParse(cache *Cache, uri string) (bool, error) { +func updateParse(cache *cache.Cache, uri string) (bool, error) { content, ok := cache.GetFileContents(uri) if !ok { return false, fmt.Errorf("failed to get file contents for uri %q", uri) @@ -101,7 +102,7 @@ func updateParse(cache *Cache, uri string) (bool, error) { return false, nil } -func updateFileDiagnostics(ctx context.Context, cache *Cache, regalConfig *config.Config, uri, rootDir string) error { +func updateFileDiagnostics(ctx context.Context, cache *cache.Cache, regalConfig *config.Config, uri, rootDir string) error { module, ok := cache.GetModule(uri) if !ok { // then there must have been a parse error @@ -183,7 +184,7 @@ func updateFileDiagnostics(ctx context.Context, cache *Cache, regalConfig *confi return nil } -func updateAllDiagnostics(ctx context.Context, cache *Cache, regalConfig *config.Config, detachedURI string) error { +func updateAllDiagnostics(ctx context.Context, cache *cache.Cache, regalConfig *config.Config, detachedURI string) error { modules := cache.GetAllModules() files := cache.GetAllFiles() diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 6c1de638..a825ee36 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -18,8 +18,10 @@ import ( "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/format" + "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/clients" "github.com/styrainc/regal/internal/lsp/commands" + "github.com/styrainc/regal/internal/lsp/completions" lsconfig "github.com/styrainc/regal/internal/lsp/config" "github.com/styrainc/regal/internal/lsp/opa/oracle" "github.com/styrainc/regal/internal/lsp/types" @@ -43,21 +45,23 @@ type LanguageServerOptions struct { } func NewLanguageServer(opts *LanguageServerOptions) *LanguageServer { + c := cache.NewCache() ls := &LanguageServer{ - cache: NewCache(), + cache: c, errorLog: opts.ErrorLog, diagnosticRequestFile: make(chan fileUpdateEvent, 10), diagnosticRequestWorkspace: make(chan string, 10), builtinsPositionFile: make(chan fileUpdateEvent, 10), commandRequest: make(chan types.ExecuteCommandParams, 10), configWatcher: lsconfig.NewWatcher(&lsconfig.WatcherOpts{ErrorWriter: opts.ErrorLog}), + completionsManager: completions.NewDefaultManager(c), } return ls } type LanguageServer struct { - cache *Cache + cache *cache.Cache conn *jsonrpc2.Conn @@ -80,6 +84,8 @@ type LanguageServer struct { // workspaceMode is set to true when the ls is initialized with // a clientRootURI. workspaceMode bool + + completionsManager *completions.Manager } // fileUpdateEvent is sent to a channel when an update is required for a file. @@ -129,6 +135,8 @@ func (l *LanguageServer) Handle( return l.handleTextDocumentHover(ctx, conn, req) case "textDocument/inlayHint": return l.handleTextDocumentInlayHint(ctx, conn, req) + case "textDocument/completion": + return l.handleTextDocumentCompletion(ctx, conn, req) case "workspace/didChangeWatchedFiles": return l.handleWorkspaceDidChangeWatchedFiles(ctx, conn, req) case "workspace/diagnostic": @@ -657,6 +665,33 @@ func (l *LanguageServer) handleTextDocumentInlayHint( return inlayHints, nil } +func (l *LanguageServer) handleTextDocumentCompletion( + _ context.Context, + _ *jsonrpc2.Conn, + req *jsonrpc2.Request, +) (result any, err error) { + var params types.CompletionParams + if err := json.Unmarshal(*req.Params, ¶ms); err != nil { + return nil, fmt.Errorf("failed to unmarshal params: %w", err) + } + + bs, err := json.MarshalIndent(params, "", " ") + l.logError(fmt.Errorf("completion params: %s", bs)) + + items, err := l.completionsManager.Run(params) + if err != nil { + return nil, fmt.Errorf("failed to find completions: %w", err) + } + + bs, err = json.MarshalIndent(items, "", " ") + l.logError(fmt.Errorf("completion items: %s", bs)) + + return types.CompletionList{ + IsIncomplete: false, + Items: items, + }, nil +} + func partialInlayHints(parseErrors []types.Diagnostic, contents, uri string) []types.InlayHint { firstErrorLine := uint(0) for _, parseError := range parseErrors { @@ -952,7 +987,7 @@ func (l *LanguageServer) handleWorkspaceDidCreateFiles( } for _, createOp := range params.Files { - _, err = updateCacheForURIFromDisk( + _, err = cache.UpdateCacheForURIFromDisk( l.cache, uri.FromPath(l.clientIdentifier, createOp.URI), uri.ToPath(l.clientIdentifier, createOp.URI), @@ -1008,7 +1043,7 @@ func (l *LanguageServer) handleWorkspaceDidRenameFiles( } for _, renameOp := range params.Files { - content, err := updateCacheForURIFromDisk( + content, err := cache.UpdateCacheForURIFromDisk( l.cache, uri.FromPath(l.clientIdentifier, renameOp.NewURI), uri.ToPath(l.clientIdentifier, renameOp.NewURI), @@ -1132,6 +1167,12 @@ func (l *LanguageServer) handleInitialize( DefinitionProvider: true, DocumentSymbolProvider: true, WorkspaceSymbolProvider: true, + CompletionProvider: types.CompletionOptions{ + ResolveProvider: false, + CompletionItem: types.CompletionItemOptions{ + LabelDetailsSupport: true, + }, + }, }, } @@ -1167,7 +1208,7 @@ func (l *LanguageServer) loadWorkspaceContents() error { fileURI := uri.FromPath(l.clientIdentifier, path) - _, err = updateCacheForURIFromDisk(l.cache, fileURI, path) + _, err = cache.UpdateCacheForURIFromDisk(l.cache, fileURI, path) if err != nil { return fmt.Errorf("failed to update cache for uri %q: %w", path, err) } diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index abdb3390..73660ce0 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -13,6 +13,7 @@ import ( "github.com/sourcegraph/jsonrpc2" + "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/types" ) @@ -514,7 +515,7 @@ func TestProcessBuiltinUpdateExitsOnMissingFile(t *testing.T) { t.Parallel() ls := LanguageServer{ - cache: NewCache(), + cache: cache.NewCache(), } err := ls.processBuiltinsUpdate(context.Background(), "file://missing.rego", "foo") @@ -522,8 +523,8 @@ func TestProcessBuiltinUpdateExitsOnMissingFile(t *testing.T) { t.Fatal(err) } - if len(ls.cache.builtinPositionsFile) != 0 { - t.Errorf("expected builtin positions to be empty, got %v", ls.cache.builtinPositionsFile) + if l := len(ls.cache.GetAllBuiltInPositions()); l != 0 { + t.Errorf("expected builtin positions to be empty, got %d items", l) } contents, ok := ls.cache.GetFileContents("file://missing.rego") diff --git a/internal/lsp/types/internal.go b/internal/lsp/types/internal.go new file mode 100644 index 00000000..c130873a --- /dev/null +++ b/internal/lsp/types/internal.go @@ -0,0 +1,10 @@ +package types + +import "github.com/open-policy-agent/opa/ast" + +type BuiltinPosition struct { + Builtin *ast.Builtin + Line uint + Start uint + End uint +} diff --git a/internal/lsp/types/types.go b/internal/lsp/types/types.go index d633e950..8ec85401 100644 --- a/internal/lsp/types/types.go +++ b/internal/lsp/types/types.go @@ -96,6 +96,48 @@ type ServerCapabilities struct { DocumentSymbolProvider bool `json:"documentSymbolProvider"` WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider"` DefinitionProvider bool `json:"definitionProvider"` + CompletionProvider CompletionOptions `json:"completionProvider"` +} + +type CompletionOptions struct { + CompletionItem CompletionItemOptions `json:"completionItem"` + ResolveProvider bool `json:"resolveProvider"` +} + +type CompletionItemOptions struct { + LabelDetailsSupport bool `json:"labelDetailsSupport"` +} + +type CompletionParams struct { + TextDocument TextDocumentIdentifier `json:"textDocument"` + Position Position `json:"position"` + Context CompletionContext `json:"context"` +} + +type CompletionContext struct { + TriggerKind uint `json:"triggerKind"` + TriggerCharacter string `json:"triggerCharacter"` +} + +type CompletionList struct { + IsIncomplete bool `json:"isIncomplete"` + Items []CompletionItem `json:"items"` +} + +type CompletionItem struct { + Label string `json:"label"` + LabelDetails *CompletionItemLabelDetails `json:"labelDetails,omitempty"` + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind + Kind uint `json:"kind"` + Detail string `json:"detail"` + Documentation *MarkupContent `json:"documentation,omitempty"` + Preselect bool `json:"preselect"` + TextEdit *TextEdit `json:"textEdit,omitempty"` +} + +type CompletionItemLabelDetails struct { + Description string `json:"description"` + Detail string `json:"detail"` } type WorkspaceOptions struct { From ffe275fbbf55036afcb201dde58d4893ca2f65b9 Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 9 May 2024 18:29:42 +0100 Subject: [PATCH 2/8] lsp/completions: add builtins and imports for v1 Signed-off-by: Charlie Egan --- internal/lsp/completions/manager.go | 3 + .../lsp/completions/providers/builtins.go | 63 +++++++++ .../completions/providers/builtins_test.go | 126 ++++++++++++++++++ internal/lsp/completions/providers/package.go | 4 +- .../lsp/completions/providers/package_test.go | 6 + .../lsp/completions/providers/packagename.go | 4 +- .../completions/providers/packagename_test.go | 6 + internal/lsp/completions/providers/regov1.go | 64 +++++++++ .../lsp/completions/providers/regov1_test.go | 49 +++++++ internal/lsp/documentsymbol.go | 3 +- internal/lsp/{ => hover}/hover.go | 42 ++---- internal/lsp/{ => hover}/hover_test.go | 4 +- .../testdata/hover/graphreachable.md | 0 .../lsp/{ => hover}/testdata/hover/indexof.md | 0 .../{ => hover}/testdata/hover/jsonfilter.md | 0 internal/lsp/inlayhint.go | 5 +- internal/lsp/rego/builtins.go | 32 +++++ internal/lsp/{ => rego}/rego.go | 6 +- internal/lsp/server.go | 5 +- 19 files changed, 375 insertions(+), 47 deletions(-) create mode 100644 internal/lsp/completions/providers/builtins.go create mode 100644 internal/lsp/completions/providers/builtins_test.go create mode 100644 internal/lsp/completions/providers/regov1.go create mode 100644 internal/lsp/completions/providers/regov1_test.go rename internal/lsp/{ => hover}/hover.go (77%) rename internal/lsp/{ => hover}/hover_test.go (90%) rename internal/lsp/{ => hover}/testdata/hover/graphreachable.md (100%) rename internal/lsp/{ => hover}/testdata/hover/indexof.md (100%) rename internal/lsp/{ => hover}/testdata/hover/jsonfilter.md (100%) create mode 100644 internal/lsp/rego/builtins.go rename internal/lsp/{ => rego}/rego.go (89%) diff --git a/internal/lsp/completions/manager.go b/internal/lsp/completions/manager.go index 06c38cc8..e14988f3 100644 --- a/internal/lsp/completions/manager.go +++ b/internal/lsp/completions/manager.go @@ -29,6 +29,8 @@ func NewDefaultManager(c *cache.Cache) *Manager { m.RegisterProvider(&providers.Package{}) m.RegisterProvider(&providers.PackageName{}) + m.RegisterProvider(&providers.BuiltIns{}) + m.RegisterProvider(&providers.RegoV1{}) return m } @@ -41,6 +43,7 @@ func (m *Manager) Run(params types.CompletionParams) ([]types.CompletionItem, er if err != nil { return nil, fmt.Errorf("error running completion provider: %w", err) } + if len(providerCompletions) > 0 { completions = append(completions, providerCompletions...) } diff --git a/internal/lsp/completions/providers/builtins.go b/internal/lsp/completions/providers/builtins.go new file mode 100644 index 00000000..c3fb78dd --- /dev/null +++ b/internal/lsp/completions/providers/builtins.go @@ -0,0 +1,63 @@ +package providers + +import ( + "strings" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/hover" + "github.com/styrainc/regal/internal/lsp/rego" + "github.com/styrainc/regal/internal/lsp/types" +) + +type BuiltIns struct{} + +func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { + + fileURI := params.TextDocument.URI + + fileContents, ok := c.GetFileContents(fileURI) + if !ok { + // if the file contents is missing then we can't provide completions + return nil, nil + } + + lines := strings.Split(fileContents, "\n") + if params.Position.Line >= uint(len(lines)) { + return nil, nil + } + + line := lines[params.Position.Line] + + if len(line) < int(params.Position.Character) || len(line) < 2 { + return nil, nil + } + + if !strings.Contains(line, " if ") && // if after if keyword + !strings.Contains(line, " contains ") && // if after contains + !strings.Contains(line, " else ") && // if after else + !strings.Contains(line, "= ") && // if after assignment + !strings.HasPrefix(line, " ") { // if in rule body + return nil, nil + } + + words := strings.Split(line, " ") + lastWord := words[len(words)-1] + + items := []types.CompletionItem{} + + for key, builtIn := range rego.BuiltIns { + if strings.HasPrefix(key, lastWord) { + items = append(items, types.CompletionItem{ + Label: key, + Kind: 3, // 3 is the kind for a function + Detail: "", + Documentation: &types.MarkupContent{ + Kind: "markdown", + Value: hover.CreateHoverContent(builtIn), + }, + }) + } + } + + return items, nil +} diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go new file mode 100644 index 00000000..7a7e08fa --- /dev/null +++ b/internal/lsp/completions/providers/builtins_test.go @@ -0,0 +1,126 @@ +package providers + +import ( + "slices" + "strings" + "testing" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/types" +) + +func TestBuiltIns_if(t *testing.T) { + t.Parallel() + + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := `package foo + +allow if c` + + c.SetFileContents(fileURI, fileContents) + + p := &BuiltIns{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 2, + Character: 10, // is the c char that triggered the request + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + labels := completionLabels(completions) + + if !slices.Contains(labels, "count") { + t.Fatalf("Expected to find 'count' in completions, got: %s", strings.Join(labels, ", ")) + } +} + +func TestBuiltIns_afterAssignment(t *testing.T) { + t.Parallel() + + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := `package foo + +allow := c` + + c.SetFileContents(fileURI, fileContents) + + p := &BuiltIns{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 2, + Character: 10, // is the c char that triggered the request + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + labels := completionLabels(completions) + + if !slices.Contains(labels, "count") { + t.Fatalf("Expected to find 'count' in completions, got: %s", strings.Join(labels, ", ")) + } +} + +func TestBuiltIns_inRuleBody(t *testing.T) { + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := `package foo + +allow if { + c +}` + + c.SetFileContents(fileURI, fileContents) + + p := &BuiltIns{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 3, + Character: 3, // is the c char that triggered the request + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + labels := completionLabels(completions) + + if !slices.Contains(labels, "count") { + t.Fatalf("Expected to find 'count' in completions, got: %s", strings.Join(labels, ", ")) + } +} + +func completionLabels(completions []types.CompletionItem) []string { + labels := make([]string, len(completions)) + for i, c := range completions { + labels[i] = c.Label + } + return labels +} diff --git a/internal/lsp/completions/providers/package.go b/internal/lsp/completions/providers/package.go index edd273ba..9f0320db 100644 --- a/internal/lsp/completions/providers/package.go +++ b/internal/lsp/completions/providers/package.go @@ -12,9 +12,9 @@ import ( // Package will return completions for the package keyword when starting a new file. type Package struct{} -func (p *Package) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { - +func (*Package) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI + fileContents, ok := c.GetFileContents(fileURI) if !ok { // if the file contents is missing then we can't provide completions diff --git a/internal/lsp/completions/providers/package_test.go b/internal/lsp/completions/providers/package_test.go index 657962e6..6ef6d736 100644 --- a/internal/lsp/completions/providers/package_test.go +++ b/internal/lsp/completions/providers/package_test.go @@ -8,6 +8,8 @@ import ( ) func TestPackage(t *testing.T) { + t.Parallel() + c := cache.NewCache() fileURI := "file:///foo/bar/file.rego" @@ -43,6 +45,8 @@ func TestPackage(t *testing.T) { } func TestPackageAfterComment(t *testing.T) { + t.Parallel() + c := cache.NewCache() fileURI := "file:///foo/bar/file.rego" @@ -82,6 +86,8 @@ p } func TestPackageNotLaterLines(t *testing.T) { + t.Parallel() + c := cache.NewCache() fileURI := "file:///foo/bar/file.rego" diff --git a/internal/lsp/completions/providers/packagename.go b/internal/lsp/completions/providers/packagename.go index 3f4f0ca5..7c36fe19 100644 --- a/internal/lsp/completions/providers/packagename.go +++ b/internal/lsp/completions/providers/packagename.go @@ -14,9 +14,9 @@ import ( // PackageName will return completions for the package name when starting a new file based on the file's URI. type PackageName struct{} -func (p *PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { - +func (*PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI + fileContents, ok := c.GetFileContents(fileURI) if !ok { // if the file contents is missing then we can't provide completions diff --git a/internal/lsp/completions/providers/packagename_test.go b/internal/lsp/completions/providers/packagename_test.go index b52eeecf..86ac5f3e 100644 --- a/internal/lsp/completions/providers/packagename_test.go +++ b/internal/lsp/completions/providers/packagename_test.go @@ -8,6 +8,8 @@ import ( ) func TestPackageName(t *testing.T) { + t.Parallel() + c := cache.NewCache() fileURI := "file:///foo/bar/file.rego" @@ -43,6 +45,8 @@ func TestPackageName(t *testing.T) { } func TestPackageNameWithPackageComment(t *testing.T) { + t.Parallel() + c := cache.NewCache() fileURI := "file:///bar/foo/file.rego" @@ -82,6 +86,8 @@ package ` } func TestPackageNameWithErroneousPackageStatements(t *testing.T) { + t.Parallel() + c := cache.NewCache() fileURI := "file:///foo/bar/file.rego" diff --git a/internal/lsp/completions/providers/regov1.go b/internal/lsp/completions/providers/regov1.go new file mode 100644 index 00000000..115dec2c --- /dev/null +++ b/internal/lsp/completions/providers/regov1.go @@ -0,0 +1,64 @@ +package providers + +import ( + "strings" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/types" +) + +type RegoV1 struct{} + +func (p *RegoV1) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { + + fileURI := params.TextDocument.URI + + fileContents, ok := c.GetFileContents(fileURI) + if !ok { + // if the file contents is missing then we can't provide completions + return nil, nil + } + + lines := strings.Split(fileContents, "\n") + if params.Position.Line >= uint(len(lines)) { + return nil, nil + } + + line := lines[params.Position.Line] + + if len(line) < int(params.Position.Character) { + return nil, nil + } + + if !strings.HasPrefix(line, "import ") { // if in rule body + return nil, nil + } + + words := strings.Split(line, " ") + lastWord := words[len(words)-1] + + if !strings.HasPrefix("rego.v1", lastWord) { + return nil, nil + } + + return []types.CompletionItem{ + { + Label: "rego.v1", + Kind: 9, // 9 is for Module + Detail: "Use Rego v1", + TextEdit: &types.TextEdit{ + Range: types.Range{ + Start: types.Position{ + Line: params.Position.Line, + Character: 7, + }, + End: types.Position{ + Line: params.Position.Line, + Character: uint(len(line)), + }, + }, + NewText: "rego.v1\n\n", + }, + }, + }, nil +} diff --git a/internal/lsp/completions/providers/regov1_test.go b/internal/lsp/completions/providers/regov1_test.go new file mode 100644 index 00000000..44cc6c69 --- /dev/null +++ b/internal/lsp/completions/providers/regov1_test.go @@ -0,0 +1,49 @@ +package providers + +import ( + "testing" + + "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/types" +) + +func TestRegoV1(t *testing.T) { + t.Parallel() + + c := cache.NewCache() + + fileURI := "file:///foo/bar/file.rego" + fileContents := `package fo + +import r + +` + + c.SetFileContents(fileURI, fileContents) + + p := &RegoV1{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 2, + Character: 8, + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 1 { + t.Fatalf("Expected exactly one completion, got: %v", completions) + } + + comp := completions[0] + if comp.Label != "rego.v1" { + t.Fatalf("Expected label to be 'package', got: %v", comp.Label) + } +} diff --git a/internal/lsp/documentsymbol.go b/internal/lsp/documentsymbol.go index d8453bd7..b64b5ad8 100644 --- a/internal/lsp/documentsymbol.go +++ b/internal/lsp/documentsymbol.go @@ -7,6 +7,7 @@ import ( "github.com/open-policy-agent/opa/ast" + "github.com/styrainc/regal/internal/lsp/rego" "github.com/styrainc/regal/internal/lsp/types" "github.com/styrainc/regal/internal/lsp/types/symbols" ) @@ -213,7 +214,7 @@ func getRuleDetail(rule *ast.Rule) string { case ast.Call: name := v[0].String() - if builtin, ok := builtins[name]; ok { + if builtin, ok := rego.BuiltIns[name]; ok { retType := builtin.Decl.NamedResult().String() detail += fmt.Sprintf(" (%s)", simplifyType(retType)) diff --git a/internal/lsp/hover.go b/internal/lsp/hover/hover.go similarity index 77% rename from internal/lsp/hover.go rename to internal/lsp/hover/hover.go index 9df05758..0b2c30ac 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover/hover.go @@ -1,4 +1,4 @@ -package lsp +package hover import ( "fmt" @@ -10,35 +10,11 @@ import ( "github.com/open-policy-agent/opa/types" "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/rego" types2 "github.com/styrainc/regal/internal/lsp/types" ) -var builtins = builtinMap() //nolint:gochecknoglobals - -var builtinHoverCache = make(map[*ast.Builtin]string) //nolint:gochecknoglobals - -func builtinMap() map[string]*ast.Builtin { - m := make(map[string]*ast.Builtin) - for _, b := range ast.CapabilitiesForThisVersion().Builtins { - m[b.Name] = b - } - - return m -} - -func builtinCategory(builtin *ast.Builtin) (category string) { - if len(builtin.Categories) == 0 { - if s := strings.Split(builtin.Name, "."); len(s) > 1 { - category = s[0] - } else { - category = builtin.Name - } - } else { - category = builtin.Categories[0] - } - - return category -} +var builtinCache = make(map[*ast.Builtin]string) //nolint:gochecknoglobals func writeFunctionSnippet(sb *strings.Builder, builtin *ast.Builtin) { sb.WriteString("```rego\n") @@ -70,15 +46,15 @@ func writeFunctionSnippet(sb *strings.Builder, builtin *ast.Builtin) { sb.WriteString(")\n```") } -func createHoverContent(builtin *ast.Builtin) string { - if content, ok := builtinHoverCache[builtin]; ok { +func CreateHoverContent(builtin *ast.Builtin) string { + if content, ok := builtinCache[builtin]; ok { return content } title := fmt.Sprintf( "[%s](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-%s-%s)", builtin.Name, - builtinCategory(builtin), + rego.BuiltinCategory(builtin), strings.ReplaceAll(builtin.Name, ".", ""), ) @@ -143,12 +119,12 @@ func createHoverContent(builtin *ast.Builtin) string { result := sb.String() - builtinHoverCache[builtin] = result + builtinCache[builtin] = result return result } -func updateBuiltinPositions(cache *cache.Cache, uri string) error { +func UpdateBuiltinPositions(cache *cache.Cache, uri string) error { module, ok := cache.GetModule(uri) if !ok { return fmt.Errorf("failed to update builtin positions: no parsed module for uri %q", uri) @@ -156,7 +132,7 @@ func updateBuiltinPositions(cache *cache.Cache, uri string) error { builtinsOnLine := map[uint][]types2.BuiltinPosition{} - for _, call := range AllBuiltinCalls(module) { + for _, call := range rego.AllBuiltinCalls(module) { line := uint(call.Location.Row) builtinsOnLine[line] = append(builtinsOnLine[line], types2.BuiltinPosition{ diff --git a/internal/lsp/hover_test.go b/internal/lsp/hover/hover_test.go similarity index 90% rename from internal/lsp/hover_test.go rename to internal/lsp/hover/hover_test.go index e8ce73f3..8b567d38 100644 --- a/internal/lsp/hover_test.go +++ b/internal/lsp/hover/hover_test.go @@ -1,4 +1,4 @@ -package lsp +package hover import ( "os" @@ -34,7 +34,7 @@ func TestCreateHoverContent(t *testing.T) { t.Fatal(err) } - hoverContent := createHoverContent(c.builtin) + hoverContent := CreateHoverContent(c.builtin) if string(file) != hoverContent { t.Errorf("Expected %s, got %s", string(file), hoverContent) diff --git a/internal/lsp/testdata/hover/graphreachable.md b/internal/lsp/hover/testdata/hover/graphreachable.md similarity index 100% rename from internal/lsp/testdata/hover/graphreachable.md rename to internal/lsp/hover/testdata/hover/graphreachable.md diff --git a/internal/lsp/testdata/hover/indexof.md b/internal/lsp/hover/testdata/hover/indexof.md similarity index 100% rename from internal/lsp/testdata/hover/indexof.md rename to internal/lsp/hover/testdata/hover/indexof.md diff --git a/internal/lsp/testdata/hover/jsonfilter.md b/internal/lsp/hover/testdata/hover/jsonfilter.md similarity index 100% rename from internal/lsp/testdata/hover/jsonfilter.md rename to internal/lsp/hover/testdata/hover/jsonfilter.md diff --git a/internal/lsp/inlayhint.go b/internal/lsp/inlayhint.go index a6f1b060..1ac7a26d 100644 --- a/internal/lsp/inlayhint.go +++ b/internal/lsp/inlayhint.go @@ -6,6 +6,7 @@ import ( "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/types" + "github.com/styrainc/regal/internal/lsp/rego" types2 "github.com/styrainc/regal/internal/lsp/types" ) @@ -20,7 +21,7 @@ func createInlayTooltip(named *types.NamedType) string { func getInlayHints(module *ast.Module) []types2.InlayHint { inlayHints := make([]types2.InlayHint, 0) - for _, call := range AllBuiltinCalls(module) { + for _, call := range rego.AllBuiltinCalls(module) { for i, arg := range call.Builtin.Decl.NamedFuncArgs().Args { if len(call.Args) <= i { // avoid panic if provided a builtin function where the args @@ -30,7 +31,7 @@ func getInlayHints(module *ast.Module) []types2.InlayHint { if named, ok := arg.(*types.NamedType); ok { inlayHints = append(inlayHints, types2.InlayHint{ - Position: positionFromLocation(call.Args[i].Location), + Position: rego.PositionFromLocation(call.Args[i].Location), Label: named.Name + ":", Kind: 2, PaddingLeft: false, diff --git a/internal/lsp/rego/builtins.go b/internal/lsp/rego/builtins.go new file mode 100644 index 00000000..ce266ae3 --- /dev/null +++ b/internal/lsp/rego/builtins.go @@ -0,0 +1,32 @@ +package rego + +import ( + "strings" + + "github.com/open-policy-agent/opa/ast" +) + +var BuiltIns = builtinMap() //nolint:gochecknoglobals + +func builtinMap() map[string]*ast.Builtin { + m := make(map[string]*ast.Builtin) + for _, b := range ast.CapabilitiesForThisVersion().Builtins { + m[b.Name] = b + } + + return m +} + +func BuiltinCategory(builtin *ast.Builtin) (category string) { + if len(builtin.Categories) == 0 { + if s := strings.Split(builtin.Name, "."); len(s) > 1 { + category = s[0] + } else { + category = builtin.Name + } + } else { + category = builtin.Categories[0] + } + + return category +} diff --git a/internal/lsp/rego.go b/internal/lsp/rego/rego.go similarity index 89% rename from internal/lsp/rego.go rename to internal/lsp/rego/rego.go index 2f7ee0cf..c6e6aeb7 100644 --- a/internal/lsp/rego.go +++ b/internal/lsp/rego/rego.go @@ -1,4 +1,4 @@ -package lsp +package rego import ( "github.com/open-policy-agent/opa/ast" @@ -12,7 +12,7 @@ type BuiltInCall struct { Args []*ast.Term } -func positionFromLocation(loc *ast.Location) types.Position { +func PositionFromLocation(loc *ast.Location) types.Position { return types.Position{ Line: uint(loc.Row - 1), Character: uint(loc.Col - 1), @@ -42,7 +42,7 @@ func AllBuiltinCalls(module *ast.Module) []BuiltInCall { return false } - if b, ok := builtins[terms[0].Value.String()]; ok { + if b, ok := BuiltIns[terms[0].Value.String()]; ok { // Exclude operators and similar builtins if b.Infix != "" { return false diff --git a/internal/lsp/server.go b/internal/lsp/server.go index a825ee36..d01ceb39 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -23,6 +23,7 @@ import ( "github.com/styrainc/regal/internal/lsp/commands" "github.com/styrainc/regal/internal/lsp/completions" lsconfig "github.com/styrainc/regal/internal/lsp/config" + "github.com/styrainc/regal/internal/lsp/hover" "github.com/styrainc/regal/internal/lsp/opa/oracle" "github.com/styrainc/regal/internal/lsp/types" "github.com/styrainc/regal/internal/lsp/uri" @@ -480,7 +481,7 @@ func (l *LanguageServer) processBuiltinsUpdate(_ context.Context, uri string, co return nil } - return updateBuiltinPositions(l.cache, uri) + return hover.UpdateBuiltinPositions(l.cache, uri) } func (l *LanguageServer) logError(err error) { @@ -515,7 +516,7 @@ func (l *LanguageServer) handleTextDocumentHover( for _, bp := range builtinsOnLine[params.Position.Line+1] { if params.Position.Character >= bp.Start-1 && params.Position.Character <= bp.End-1 { - contents := createHoverContent(bp.Builtin) + contents := hover.CreateHoverContent(bp.Builtin) return HoverResponse{ Contents: types.MarkupContent{ From fecf0b494e616969201cb9c2e989343f5ccdd71a Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Wed, 15 May 2024 13:21:54 +0100 Subject: [PATCH 3/8] lsp: fix linter errors Signed-off-by: Charlie Egan --- internal/lsp/completions/manager_test.go | 3 ++- internal/lsp/completions/providers/builtins.go | 1 - .../lsp/completions/providers/builtins_test.go | 6 +++--- internal/lsp/completions/providers/package.go | 13 +++++-------- .../lsp/completions/providers/package_test.go | 3 --- internal/lsp/completions/providers/packagename.go | 2 +- .../lsp/completions/providers/packagename_test.go | 1 - internal/lsp/completions/providers/regov1.go | 4 ++-- internal/lsp/completions/providers/shared.go | 3 +++ internal/lsp/lint.go | 15 +++++++++++++-- internal/lsp/server.go | 13 ++++++------- 11 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 internal/lsp/completions/providers/shared.go diff --git a/internal/lsp/completions/manager_test.go b/internal/lsp/completions/manager_test.go index 7ff6d379..70b49f3e 100644 --- a/internal/lsp/completions/manager_test.go +++ b/internal/lsp/completions/manager_test.go @@ -9,8 +9,9 @@ import ( ) func TestManager(t *testing.T) { - c := cache.NewCache() + t.Parallel() + c := cache.NewCache() fileURI := "file:///foo/bar/file.rego" fileContents := "" diff --git a/internal/lsp/completions/providers/builtins.go b/internal/lsp/completions/providers/builtins.go index c3fb78dd..7977e2c6 100644 --- a/internal/lsp/completions/providers/builtins.go +++ b/internal/lsp/completions/providers/builtins.go @@ -12,7 +12,6 @@ import ( type BuiltIns struct{} func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { - fileURI := params.TextDocument.URI fileContents, ok := c.GetFileContents(fileURI) diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go index 7a7e08fa..fa0edbee 100644 --- a/internal/lsp/completions/providers/builtins_test.go +++ b/internal/lsp/completions/providers/builtins_test.go @@ -14,7 +14,6 @@ func TestBuiltIns_if(t *testing.T) { c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := `package foo allow if c` @@ -50,7 +49,6 @@ func TestBuiltIns_afterAssignment(t *testing.T) { c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := `package foo allow := c` @@ -82,9 +80,10 @@ allow := c` } func TestBuiltIns_inRuleBody(t *testing.T) { + t.Parallel() + c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := `package foo allow if { @@ -122,5 +121,6 @@ func completionLabels(completions []types.CompletionItem) []string { for i, c := range completions { labels[i] = c.Label } + return labels } diff --git a/internal/lsp/completions/providers/package.go b/internal/lsp/completions/providers/package.go index 9f0320db..9d963974 100644 --- a/internal/lsp/completions/providers/package.go +++ b/internal/lsp/completions/providers/package.go @@ -1,8 +1,6 @@ package providers import ( - "fmt" - "os" "strings" "github.com/styrainc/regal/internal/lsp/cache" @@ -18,30 +16,29 @@ func (*Package) Run(c *cache.Cache, params types.CompletionParams) ([]types.Comp fileContents, ok := c.GetFileContents(fileURI) if !ok { // if the file contents is missing then we can't provide completions - return nil, nil + return []types.CompletionItem{}, nil } lines := strings.Split(fileContents, "\n") if len(lines) < 1 { - return nil, nil + return []types.CompletionItem{}, nil } for i, line := range lines { if i < int(params.Position.Line) && strings.HasPrefix(line, "package ") { // if there is already a package statement in the file then we don't provide any more completions - return nil, nil + return []types.CompletionItem{}, nil } } // if we can't confirm that the user has package statement on the line then we don't provide completions if len(lines)+1 < int(params.Position.Line) { - fmt.Fprintln(os.Stderr, "no package statement") - return nil, nil + return []types.CompletionItem{}, nil } // if not on the first line, the user must type p before we provide completions if params.Position.Line != 0 && !strings.HasPrefix(lines[params.Position.Line], "p") { - return nil, nil + return []types.CompletionItem{}, nil } return []types.CompletionItem{ diff --git a/internal/lsp/completions/providers/package_test.go b/internal/lsp/completions/providers/package_test.go index 6ef6d736..a0889142 100644 --- a/internal/lsp/completions/providers/package_test.go +++ b/internal/lsp/completions/providers/package_test.go @@ -12,7 +12,6 @@ func TestPackage(t *testing.T) { c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := "\n" c.SetFileContents(fileURI, fileContents) @@ -49,7 +48,6 @@ func TestPackageAfterComment(t *testing.T) { c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := ` # this is a comment before the package statement p @@ -90,7 +88,6 @@ func TestPackageNotLaterLines(t *testing.T) { c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := "package foo\n\n" c.SetFileContents(fileURI, fileContents) diff --git a/internal/lsp/completions/providers/packagename.go b/internal/lsp/completions/providers/packagename.go index 7c36fe19..6ac3464d 100644 --- a/internal/lsp/completions/providers/packagename.go +++ b/internal/lsp/completions/providers/packagename.go @@ -49,7 +49,7 @@ func (*PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types. return []types.CompletionItem{ { - Label: fmt.Sprintf("package %s", dir), + Label: "package " + dir, Detail: "suggested package name based on directory", Kind: 19, // 19 is the kind for a folder TextEdit: &types.TextEdit{ diff --git a/internal/lsp/completions/providers/packagename_test.go b/internal/lsp/completions/providers/packagename_test.go index 86ac5f3e..3c96a0ca 100644 --- a/internal/lsp/completions/providers/packagename_test.go +++ b/internal/lsp/completions/providers/packagename_test.go @@ -12,7 +12,6 @@ func TestPackageName(t *testing.T) { c := cache.NewCache() - fileURI := "file:///foo/bar/file.rego" fileContents := "package " c.SetFileContents(fileURI, fileContents) diff --git a/internal/lsp/completions/providers/regov1.go b/internal/lsp/completions/providers/regov1.go index 115dec2c..9b273569 100644 --- a/internal/lsp/completions/providers/regov1.go +++ b/internal/lsp/completions/providers/regov1.go @@ -9,8 +9,7 @@ import ( type RegoV1 struct{} -func (p *RegoV1) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { - +func (*RegoV1) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI fileContents, ok := c.GetFileContents(fileURI) @@ -37,6 +36,7 @@ func (p *RegoV1) Run(c *cache.Cache, params types.CompletionParams) ([]types.Com words := strings.Split(line, " ") lastWord := words[len(words)-1] + //nolint:gocritic if !strings.HasPrefix("rego.v1", lastWord) { return nil, nil } diff --git a/internal/lsp/completions/providers/shared.go b/internal/lsp/completions/providers/shared.go new file mode 100644 index 00000000..0822c378 --- /dev/null +++ b/internal/lsp/completions/providers/shared.go @@ -0,0 +1,3 @@ +package providers + +const fileURI = "file:///foo/bar/file.rego" diff --git a/internal/lsp/lint.go b/internal/lsp/lint.go index 71d105b4..48b889dc 100644 --- a/internal/lsp/lint.go +++ b/internal/lsp/lint.go @@ -102,7 +102,13 @@ func updateParse(cache *cache.Cache, uri string) (bool, error) { return false, nil } -func updateFileDiagnostics(ctx context.Context, cache *cache.Cache, regalConfig *config.Config, uri, rootDir string) error { +func updateFileDiagnostics( + ctx context.Context, + cache *cache.Cache, + regalConfig *config.Config, + uri string, + rootDir string, +) error { module, ok := cache.GetModule(uri) if !ok { // then there must have been a parse error @@ -184,7 +190,12 @@ func updateFileDiagnostics(ctx context.Context, cache *cache.Cache, regalConfig return nil } -func updateAllDiagnostics(ctx context.Context, cache *cache.Cache, regalConfig *config.Config, detachedURI string) error { +func updateAllDiagnostics( + ctx context.Context, + cache *cache.Cache, + regalConfig *config.Config, + detachedURI string, +) error { modules := cache.GetAllModules() files := cache.GetAllFiles() diff --git a/internal/lsp/server.go b/internal/lsp/server.go index d01ceb39..98f81fb7 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -481,7 +481,12 @@ func (l *LanguageServer) processBuiltinsUpdate(_ context.Context, uri string, co return nil } - return hover.UpdateBuiltinPositions(l.cache, uri) + err = hover.UpdateBuiltinPositions(l.cache, uri) + if err != nil { + return fmt.Errorf("failed to update builtin positions: %w", err) + } + + return nil } func (l *LanguageServer) logError(err error) { @@ -676,17 +681,11 @@ func (l *LanguageServer) handleTextDocumentCompletion( return nil, fmt.Errorf("failed to unmarshal params: %w", err) } - bs, err := json.MarshalIndent(params, "", " ") - l.logError(fmt.Errorf("completion params: %s", bs)) - items, err := l.completionsManager.Run(params) if err != nil { return nil, fmt.Errorf("failed to find completions: %w", err) } - bs, err = json.MarshalIndent(items, "", " ") - l.logError(fmt.Errorf("completion items: %s", bs)) - return types.CompletionList{ IsIncomplete: false, Items: items, From cdf8868527c9b408aa5059ec22e0d535e249a3e5 Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Wed, 15 May 2024 14:06:51 +0100 Subject: [PATCH 4/8] lsp: Skip infix completions We had been showing these in places where it'd not be suitable to use them Signed-off-by: Charlie Egan --- .../lsp/completions/providers/builtins.go | 4 +++ .../completions/providers/builtins_test.go | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/internal/lsp/completions/providers/builtins.go b/internal/lsp/completions/providers/builtins.go index 7977e2c6..267855d1 100644 --- a/internal/lsp/completions/providers/builtins.go +++ b/internal/lsp/completions/providers/builtins.go @@ -45,6 +45,10 @@ func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams) ([]types.Com items := []types.CompletionItem{} for key, builtIn := range rego.BuiltIns { + if builtIn.Infix != "" { + continue + } + if strings.HasPrefix(key, lastWord) { items = append(items, types.CompletionItem{ Label: key, diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go index fa0edbee..0ed38ad0 100644 --- a/internal/lsp/completions/providers/builtins_test.go +++ b/internal/lsp/completions/providers/builtins_test.go @@ -116,6 +116,39 @@ allow if { } } +func TestBuiltIns_noInfix(t *testing.T) { + t.Parallel() + + c := cache.NewCache() + + fileContents := `package foo + +allow if gt` + + c.SetFileContents(fileURI, fileContents) + + p := &BuiltIns{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 2, + Character: 10, // is the c char that triggered the request + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(completions) != 0 { + t.Fatalf("Expected no completions, got: %v", completions) + } +} + func completionLabels(completions []types.CompletionItem) []string { labels := make([]string, len(completions)) for i, c := range completions { From 3dd9895f2609bba7e9bbe3c4c6a105ec4457ac5f Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Wed, 15 May 2024 14:36:46 +0100 Subject: [PATCH 5/8] lsp: exclude deprecated builtins Signed-off-by: Charlie Egan --- .../lsp/completions/providers/builtins.go | 4 +++ .../completions/providers/builtins_test.go | 35 +++++++++++++++++++ internal/lsp/hover/hover.go | 9 +++++ 3 files changed, 48 insertions(+) diff --git a/internal/lsp/completions/providers/builtins.go b/internal/lsp/completions/providers/builtins.go index 267855d1..3c54c728 100644 --- a/internal/lsp/completions/providers/builtins.go +++ b/internal/lsp/completions/providers/builtins.go @@ -49,6 +49,10 @@ func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams) ([]types.Com continue } + if builtIn.IsDeprecated() { + continue + } + if strings.HasPrefix(key, lastWord) { items = append(items, types.CompletionItem{ Label: key, diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go index 0ed38ad0..ae119d5f 100644 --- a/internal/lsp/completions/providers/builtins_test.go +++ b/internal/lsp/completions/providers/builtins_test.go @@ -149,6 +149,41 @@ allow if gt` } } +func TestBuiltIns_noDeprecated(t *testing.T) { + t.Parallel() + + c := cache.NewCache() + + fileContents := `package foo + +allow if c` + + c.SetFileContents(fileURI, fileContents) + + p := &BuiltIns{} + + completionParams := types.CompletionParams{ + TextDocument: types.TextDocumentIdentifier{ + URI: fileURI, + }, + Position: types.Position{ + Line: 2, + Character: 10, // is the c char that triggered the request + }, + } + + completions, err := p.Run(c, completionParams) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + labels := completionLabels(completions) + + if slices.Contains(labels, "cast_set") { + t.Fatalf("Expected no deprecated completions, got: %s", strings.Join(labels, ", ")) + } +} + func completionLabels(completions []types.CompletionItem) []string { labels := make([]string, len(completions)) for i, c := range completions { diff --git a/internal/lsp/hover/hover.go b/internal/lsp/hover/hover.go index 0b2c30ac..43fd95ac 100644 --- a/internal/lsp/hover/hover.go +++ b/internal/lsp/hover/hover.go @@ -3,6 +3,7 @@ package hover import ( "fmt" "strings" + "sync" "github.com/olekukonko/tablewriter" @@ -16,6 +17,8 @@ import ( var builtinCache = make(map[*ast.Builtin]string) //nolint:gochecknoglobals +var builtinCacheLock = &sync.Mutex{} //nolint:gochecknoglobals + func writeFunctionSnippet(sb *strings.Builder, builtin *ast.Builtin) { sb.WriteString("```rego\n") @@ -47,9 +50,13 @@ func writeFunctionSnippet(sb *strings.Builder, builtin *ast.Builtin) { } func CreateHoverContent(builtin *ast.Builtin) string { + builtinCacheLock.Lock() if content, ok := builtinCache[builtin]; ok { + builtinCacheLock.Unlock() + return content } + builtinCacheLock.Unlock() title := fmt.Sprintf( "[%s](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-%s-%s)", @@ -119,7 +126,9 @@ func CreateHoverContent(builtin *ast.Builtin) string { result := sb.String() + builtinCacheLock.Lock() builtinCache[builtin] = result + builtinCacheLock.Unlock() return result } From 166faa0da9c44324714a3bc28c2d3bcc5d611d80 Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 16 May 2024 10:31:45 +0100 Subject: [PATCH 6/8] Make testCaseFileURI clear Signed-off-by: Charlie Egan --- .../completions/providers/builtins_test.go | 20 +++++++++---------- .../lsp/completions/providers/package_test.go | 12 +++++------ .../completions/providers/packagename_test.go | 4 ++-- internal/lsp/completions/providers/shared.go | 3 --- .../lsp/completions/providers/shared_test.go | 4 ++++ 5 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 internal/lsp/completions/providers/shared.go create mode 100644 internal/lsp/completions/providers/shared_test.go diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go index ae119d5f..dc3b7da2 100644 --- a/internal/lsp/completions/providers/builtins_test.go +++ b/internal/lsp/completions/providers/builtins_test.go @@ -18,13 +18,13 @@ func TestBuiltIns_if(t *testing.T) { allow if c` - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &BuiltIns{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 2, @@ -53,13 +53,13 @@ func TestBuiltIns_afterAssignment(t *testing.T) { allow := c` - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &BuiltIns{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 2, @@ -90,13 +90,13 @@ allow if { c }` - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &BuiltIns{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 3, @@ -125,13 +125,13 @@ func TestBuiltIns_noInfix(t *testing.T) { allow if gt` - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &BuiltIns{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 2, @@ -158,13 +158,13 @@ func TestBuiltIns_noDeprecated(t *testing.T) { allow if c` - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &BuiltIns{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 2, diff --git a/internal/lsp/completions/providers/package_test.go b/internal/lsp/completions/providers/package_test.go index a0889142..3261f837 100644 --- a/internal/lsp/completions/providers/package_test.go +++ b/internal/lsp/completions/providers/package_test.go @@ -14,13 +14,13 @@ func TestPackage(t *testing.T) { fileContents := "\n" - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &Package{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 0, @@ -54,13 +54,13 @@ p ` - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &Package{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 2, @@ -90,13 +90,13 @@ func TestPackageNotLaterLines(t *testing.T) { fileContents := "package foo\n\n" - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &Package{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 1, diff --git a/internal/lsp/completions/providers/packagename_test.go b/internal/lsp/completions/providers/packagename_test.go index 3c96a0ca..c8729757 100644 --- a/internal/lsp/completions/providers/packagename_test.go +++ b/internal/lsp/completions/providers/packagename_test.go @@ -14,13 +14,13 @@ func TestPackageName(t *testing.T) { fileContents := "package " - c.SetFileContents(fileURI, fileContents) + c.SetFileContents(testCaseFileURI, fileContents) p := &PackageName{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: fileURI, + URI: testCaseFileURI, }, Position: types.Position{ Line: 0, diff --git a/internal/lsp/completions/providers/shared.go b/internal/lsp/completions/providers/shared.go deleted file mode 100644 index 0822c378..00000000 --- a/internal/lsp/completions/providers/shared.go +++ /dev/null @@ -1,3 +0,0 @@ -package providers - -const fileURI = "file:///foo/bar/file.rego" diff --git a/internal/lsp/completions/providers/shared_test.go b/internal/lsp/completions/providers/shared_test.go new file mode 100644 index 00000000..28d23504 --- /dev/null +++ b/internal/lsp/completions/providers/shared_test.go @@ -0,0 +1,4 @@ +package providers + +// testCaseFileURI is used in various tests in the providers package. +const testCaseFileURI = "file:///foo/bar/file.rego" From 37e711cbe1289749707b0c6fc75ef0c037bccdf3 Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 16 May 2024 10:36:05 +0100 Subject: [PATCH 7/8] Explain rego.v1 logic Signed-off-by: Charlie Egan --- internal/lsp/completions/providers/regov1.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/lsp/completions/providers/regov1.go b/internal/lsp/completions/providers/regov1.go index 9b273569..4eaad99b 100644 --- a/internal/lsp/completions/providers/regov1.go +++ b/internal/lsp/completions/providers/regov1.go @@ -29,13 +29,17 @@ func (*RegoV1) Run(c *cache.Cache, params types.CompletionParams) ([]types.Compl return nil, nil } - if !strings.HasPrefix(line, "import ") { // if in rule body + // this completion provider applies on lines with import at the start + if !strings.HasPrefix(line, "import ") { return nil, nil } words := strings.Split(line, " ") lastWord := words[len(words)-1] + // We might be checking lines at this point like 'import r', 'import rego', 'import rego.v', + // so here we take the last word (i.e. 'r', 'rego', 'rego.v') and check if that words is a + // prefix of 'rego.v1'. //nolint:gocritic if !strings.HasPrefix("rego.v1", lastWord) { return nil, nil From c163642ec742701a0866d417140003278b5f957e Mon Sep 17 00:00:00 2001 From: Charlie Egan Date: Thu, 16 May 2024 11:19:20 +0100 Subject: [PATCH 8/8] Suggest based on full package path Signed-off-by: Charlie Egan --- internal/lsp/completions/manager.go | 6 +++--- internal/lsp/completions/manager_test.go | 2 +- internal/lsp/completions/providers/builtins.go | 2 +- .../lsp/completions/providers/builtins_test.go | 10 +++++----- internal/lsp/completions/providers/options.go | 5 +++++ internal/lsp/completions/providers/package.go | 2 +- .../lsp/completions/providers/package_test.go | 6 +++--- .../lsp/completions/providers/packagename.go | 17 +++++++++++++---- .../completions/providers/packagename_test.go | 18 +++++++++++------- internal/lsp/completions/providers/regov1.go | 2 +- .../lsp/completions/providers/regov1_test.go | 2 +- internal/lsp/server.go | 3 ++- 12 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 internal/lsp/completions/providers/options.go diff --git a/internal/lsp/completions/manager.go b/internal/lsp/completions/manager.go index e14988f3..99bf2d8a 100644 --- a/internal/lsp/completions/manager.go +++ b/internal/lsp/completions/manager.go @@ -17,7 +17,7 @@ type Manager struct { type ManagerOptions struct{} type Provider interface { - Run(*cache.Cache, types.CompletionParams) ([]types.CompletionItem, error) + Run(*cache.Cache, types.CompletionParams, *providers.Options) ([]types.CompletionItem, error) } func NewManager(c *cache.Cache, opts *ManagerOptions) *Manager { @@ -35,11 +35,11 @@ func NewDefaultManager(c *cache.Cache) *Manager { return m } -func (m *Manager) Run(params types.CompletionParams) ([]types.CompletionItem, error) { +func (m *Manager) Run(params types.CompletionParams, opts *providers.Options) ([]types.CompletionItem, error) { var completions []types.CompletionItem for _, provider := range m.providers { - providerCompletions, err := provider.Run(m.c, params) + providerCompletions, err := provider.Run(m.c, params, opts) if err != nil { return nil, fmt.Errorf("error running completion provider: %w", err) } diff --git a/internal/lsp/completions/manager_test.go b/internal/lsp/completions/manager_test.go index 70b49f3e..9ee758d1 100644 --- a/internal/lsp/completions/manager_test.go +++ b/internal/lsp/completions/manager_test.go @@ -30,7 +30,7 @@ func TestManager(t *testing.T) { }, } - completions, err := mgr.Run(completionParams) + completions, err := mgr.Run(completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/internal/lsp/completions/providers/builtins.go b/internal/lsp/completions/providers/builtins.go index 3c54c728..1bbacbfd 100644 --- a/internal/lsp/completions/providers/builtins.go +++ b/internal/lsp/completions/providers/builtins.go @@ -11,7 +11,7 @@ import ( type BuiltIns struct{} -func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { +func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams, _ *Options) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI fileContents, ok := c.GetFileContents(fileURI) diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go index dc3b7da2..f62544c2 100644 --- a/internal/lsp/completions/providers/builtins_test.go +++ b/internal/lsp/completions/providers/builtins_test.go @@ -32,7 +32,7 @@ allow if c` }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -67,7 +67,7 @@ allow := c` }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -104,7 +104,7 @@ allow if { }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -139,7 +139,7 @@ allow if gt` }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -172,7 +172,7 @@ allow if c` }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/internal/lsp/completions/providers/options.go b/internal/lsp/completions/providers/options.go new file mode 100644 index 00000000..e4757adc --- /dev/null +++ b/internal/lsp/completions/providers/options.go @@ -0,0 +1,5 @@ +package providers + +type Options struct { + RootURI string +} diff --git a/internal/lsp/completions/providers/package.go b/internal/lsp/completions/providers/package.go index 9d963974..84392513 100644 --- a/internal/lsp/completions/providers/package.go +++ b/internal/lsp/completions/providers/package.go @@ -10,7 +10,7 @@ import ( // Package will return completions for the package keyword when starting a new file. type Package struct{} -func (*Package) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { +func (*Package) Run(c *cache.Cache, params types.CompletionParams, _ *Options) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI fileContents, ok := c.GetFileContents(fileURI) diff --git a/internal/lsp/completions/providers/package_test.go b/internal/lsp/completions/providers/package_test.go index 3261f837..714312a7 100644 --- a/internal/lsp/completions/providers/package_test.go +++ b/internal/lsp/completions/providers/package_test.go @@ -28,7 +28,7 @@ func TestPackage(t *testing.T) { }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -68,7 +68,7 @@ p }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -104,7 +104,7 @@ func TestPackageNotLaterLines(t *testing.T) { }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/internal/lsp/completions/providers/packagename.go b/internal/lsp/completions/providers/packagename.go index 6ac3464d..50118037 100644 --- a/internal/lsp/completions/providers/packagename.go +++ b/internal/lsp/completions/providers/packagename.go @@ -14,7 +14,7 @@ import ( // PackageName will return completions for the package name when starting a new file based on the file's URI. type PackageName struct{} -func (*PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { +func (*PackageName) Run(c *cache.Cache, params types.CompletionParams, opts *Options) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI fileContents, ok := c.GetFileContents(fileURI) @@ -41,7 +41,16 @@ func (*PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types. } path := uri.ToPath(clients.IdentifierGeneric, fileURI) - dir := filepath.Base(filepath.Dir(path)) + suggestedPackageName := filepath.Base(filepath.Dir(path)) + + if opts != nil { + if trimmed := strings.TrimPrefix(fileURI, opts.RootURI); trimmed != fileURI { + dir := filepath.Dir(trimmed) + noLeadingSlash := strings.TrimPrefix(dir, "/") + noPeriods := strings.ReplaceAll(noLeadingSlash, ".", "_") + suggestedPackageName = strings.ReplaceAll(noPeriods, "/", ".") + } + } if !strings.HasPrefix(lines[params.Position.Line], "p") { return nil, nil @@ -49,7 +58,7 @@ func (*PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types. return []types.CompletionItem{ { - Label: "package " + dir, + Label: "package " + suggestedPackageName, Detail: "suggested package name based on directory", Kind: 19, // 19 is the kind for a folder TextEdit: &types.TextEdit{ @@ -63,7 +72,7 @@ func (*PackageName) Run(c *cache.Cache, params types.CompletionParams) ([]types. Character: params.Position.Character, }, }, - NewText: fmt.Sprintf("package %s\n\n", dir), + NewText: fmt.Sprintf("package %s\n\n", suggestedPackageName), }, }, }, nil diff --git a/internal/lsp/completions/providers/packagename_test.go b/internal/lsp/completions/providers/packagename_test.go index c8729757..70828b72 100644 --- a/internal/lsp/completions/providers/packagename_test.go +++ b/internal/lsp/completions/providers/packagename_test.go @@ -14,13 +14,15 @@ func TestPackageName(t *testing.T) { fileContents := "package " - c.SetFileContents(testCaseFileURI, fileContents) + fileURI := "file:///foo/bar/baz/bax/file.rego" + + c.SetFileContents(fileURI, fileContents) p := &PackageName{} completionParams := types.CompletionParams{ TextDocument: types.TextDocumentIdentifier{ - URI: testCaseFileURI, + URI: fileURI, }, Position: types.Position{ Line: 0, @@ -28,7 +30,9 @@ func TestPackageName(t *testing.T) { }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, &Options{ + RootURI: "file:///foo/bar", + }) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -38,8 +42,8 @@ func TestPackageName(t *testing.T) { } comp := completions[0] - if comp.Label != "package bar" { - t.Fatalf("Expected label to be 'bar', got: %v", comp.Label) + if comp.Label != "package baz.bax" { + t.Fatalf("Expected label to be 'baz.bax', got: %v", comp.Label) } } @@ -69,7 +73,7 @@ package ` }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, &Options{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -108,7 +112,7 @@ package ` }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, &Options{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/internal/lsp/completions/providers/regov1.go b/internal/lsp/completions/providers/regov1.go index 4eaad99b..0e1dc3e6 100644 --- a/internal/lsp/completions/providers/regov1.go +++ b/internal/lsp/completions/providers/regov1.go @@ -9,7 +9,7 @@ import ( type RegoV1 struct{} -func (*RegoV1) Run(c *cache.Cache, params types.CompletionParams) ([]types.CompletionItem, error) { +func (*RegoV1) Run(c *cache.Cache, params types.CompletionParams, _ *Options) ([]types.CompletionItem, error) { fileURI := params.TextDocument.URI fileContents, ok := c.GetFileContents(fileURI) diff --git a/internal/lsp/completions/providers/regov1_test.go b/internal/lsp/completions/providers/regov1_test.go index 44cc6c69..d2a3cbf5 100644 --- a/internal/lsp/completions/providers/regov1_test.go +++ b/internal/lsp/completions/providers/regov1_test.go @@ -33,7 +33,7 @@ import r }, } - completions, err := p.Run(c, completionParams) + completions, err := p.Run(c, completionParams, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 98f81fb7..e5cee959 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -22,6 +22,7 @@ import ( "github.com/styrainc/regal/internal/lsp/clients" "github.com/styrainc/regal/internal/lsp/commands" "github.com/styrainc/regal/internal/lsp/completions" + "github.com/styrainc/regal/internal/lsp/completions/providers" lsconfig "github.com/styrainc/regal/internal/lsp/config" "github.com/styrainc/regal/internal/lsp/hover" "github.com/styrainc/regal/internal/lsp/opa/oracle" @@ -681,7 +682,7 @@ func (l *LanguageServer) handleTextDocumentCompletion( return nil, fmt.Errorf("failed to unmarshal params: %w", err) } - items, err := l.completionsManager.Run(params) + items, err := l.completionsManager.Run(params, &providers.Options{RootURI: l.clientRootURI}) if err != nil { return nil, fmt.Errorf("failed to find completions: %w", err) }