diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7b0a6..1463ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog for rust-notebook-language-server +## 0.2.7.0 + +* Transform textDocument/selectionRange, textDocument/semanticTokens and the goto family, which reached the client in shadow coordinates + ## 0.2.6.1 * Untransform workspace/applyEdit, which named the shadow document and so was refused by the client diff --git a/package.yaml b/package.yaml index 13ce595..972224d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: rust-notebook-language-server -version: 0.2.6.1 +version: 0.2.7.0 extra-source-files: - README.md @@ -80,8 +80,11 @@ tests: - -with-rtsopts=-N - -O2 - -W + build-tools: sandwich:sandwich-discover dependencies: + - containers - exceptions + - mtl - myers-diff - notebook-language-server-core - QuickCheck diff --git a/rust-notebook-language-server.cabal b/rust-notebook-language-server.cabal index 392d4d8..db176f3 100644 --- a/rust-notebook-language-server.cabal +++ b/rust-notebook-language-server.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: rust-notebook-language-server -version: 0.2.6.1 +version: 0.2.7.0 build-type: Simple extra-source-files: README.md @@ -100,6 +100,8 @@ test-suite rust-notebook-language-server-test type: exitcode-stdio-1.0 main-is: Main.hs other-modules: + Test + Test.CodeAction Test.Formatting Test.Hover Test.Transformer.HeadTail @@ -107,6 +109,7 @@ test-suite rust-notebook-language-server-test TestLib.Common TestLib.Core TestLib.Generators + TestLib.Transformers TestLib.Util Paths_rust_notebook_language_server hs-source-dirs: @@ -127,12 +130,16 @@ test-suite rust-notebook-language-server-test NumericUnderscores TypeApplications ghc-options: -Wunused-packages -Wall -fno-warn-name-shadowing -threaded -rtsopts -with-rtsopts=-N -O2 -W + build-tool-depends: + sandwich:sandwich-discover build-depends: QuickCheck , base >=4.7 && <5 + , containers , exceptions , lsp-types >=2.0.0.1 , monad-logger + , mtl , myers-diff , notebook-language-server-core , quickcheck-instances diff --git a/stack.yaml b/stack.yaml index 748b181..2ebc2a5 100644 --- a/stack.yaml +++ b/stack.yaml @@ -13,7 +13,7 @@ system-ghc: true extra-deps: - git: https://github.com/codedownio/notebook-language-server-core.git - commit: 244731900dee065cbf794ba74294f3a0f55c74fd + commit: b10c0d26debbea7ed97327eb8fff9bdb95571921 # - language-rust-0.2.0.27 diff --git a/stack.yaml.lock b/stack.yaml.lock index f463c0f..3030fef 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -5,15 +5,15 @@ packages: - completed: - commit: 244731900dee065cbf794ba74294f3a0f55c74fd + commit: b10c0d26debbea7ed97327eb8fff9bdb95571921 git: https://github.com/codedownio/notebook-language-server-core.git name: notebook-language-server-core pantry-tree: - sha256: 53abc5c9405647f56c1bd03ca3f1f29d0b9c5c7745ed39a89c7946aaced27f00 - size: 2399 + sha256: 0e014e799318cf86e866796395c8e50766903b11da434d895069831794748203 + size: 2721 version: 0.1.0.0 original: - commit: 244731900dee065cbf794ba74294f3a0f55c74fd + commit: b10c0d26debbea7ed97327eb8fff9bdb95571921 git: https://github.com/codedownio/notebook-language-server-core.git snapshots: - completed: diff --git a/test/Main.hs b/test/Main.hs index 5b0443f..fb386ce 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -1,20 +1,7 @@ +module Main (main) where -import Test.Sandwich +import qualified Test -import qualified Test.Formatting -import qualified Test.Hover - -import qualified Test.Transformer.HeadTail -import qualified Test.Transformer.StripDirective - - -spec :: TopSpec -spec = do - Test.Formatting.spec - Test.Hover.spec - - Test.Transformer.HeadTail.spec - Test.Transformer.StripDirective.spec main :: IO () -main = runSandwichWithCommandLineArgs defaultOptions spec +main = Test.main diff --git a/test/Test.hs b/test/Test.hs new file mode 100644 index 0000000..660574d --- /dev/null +++ b/test/Test.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE CPP #-} +{-# OPTIONS_GHC -F -pgmF sandwich-discover #-} +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +-- The generated tree passes module file names as string literals +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module Test (tests, main) where + +import Test.Sandwich + +#insert_test_imports + + +tests :: TopSpec +tests = $(getSpecFromFolder defaultGetSpecFromFolderOptions) + +main :: IO () +main = runSandwichWithCommandLineArgs defaultOptions tests diff --git a/test/Test/CodeAction.hs b/test/Test/CodeAction.hs new file mode 100644 index 0000000..cda994b --- /dev/null +++ b/test/Test/CodeAction.hs @@ -0,0 +1,168 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} + +-- | The code action response path, run through the real 'RustNotebookTransformer' rather than a +-- stand-in. rust-analyzer answers in the shadow document's coordinates, and the shadow document +-- is the notebook's lines wrapped in @fn main() {@, so everything it says is a line too low. +module Test.CodeAction (tests, main) where + +import Control.Monad.IO.Class +import Control.Monad.Logger +import Control.Monad.Reader +import qualified Data.Map as M +import Data.Text (Text) +import Language.LSP.NotebookLanguageServer.Transform.ServerRsp +import Language.LSP.Protocol.Message +import Language.LSP.Protocol.Types +import Test.Sandwich +import TestLib.Transformers +import Transform.Config + + +tests :: TopSpec +tests = describe "CodeAction" $ do + describe "textDocument/codeAction" $ do + -- "Insert explicit type `f64`" on `let pi = 3.14;` came back with the edit on the line + -- below, so the text landed on the following line of the cell. + it "puts an inline edit back on the notebook's line" $ do + let answered = [InR (action (Just (documentChangesEdit notebookShadowUri [InL (TextEdit (lineRange 8) ": f64")])))] + runCodeAction (askedAbout notebookUri 7) (InL answered) + >>= (`shouldBe` InL [InR (action (Just (documentChangesEdit notebookUri [InL (TextEdit (lineRange 7) ": f64")])))]) + + it "does the same for an edit given as a changes map" $ do + let answered = [InR (action (Just (changesEdit notebookShadowUri [TextEdit (lineRange 8) ": f64"])))] + runCodeAction (askedAbout notebookUri 7) (InL answered) + >>= (`shouldBe` InL [InR (action (Just (changesEdit notebookUri [TextEdit (lineRange 7) ": f64"])))]) + + -- Shadow lines 8 and 9 are the last two lines of the cell. Several edits at once, on more + -- than one line, so a transformer that mapped everything to one line wouldn't pass. + it "maps every edit in the action, each to its own line" $ do + let given = [InL (TextEdit (lineRange 8) ": f64"), InL (TextEdit (lineRange 9) "// ")] + let expected = [InL (TextEdit (lineRange 7) ": f64"), InL (TextEdit (lineRange 8) "// ")] + runCodeAction (askedAbout notebookUri 7) (InL [InR (action (Just (documentChangesEdit notebookShadowUri given)))]) + >>= (`shouldBe` InL [InR (action (Just (documentChangesEdit notebookUri expected)))]) + + it "leaves an action with no edit for resolve to fill in" $ do + let answered = [InR (action Nothing)] + runCodeAction (askedAbout notebookUri 7) (InL answered) >>= (`shouldBe` InL answered) + + it "passes a plain command through" $ do + let answered = [InL (Command "Run" "rust-analyzer.run" Nothing)] + runCodeAction (askedAbout notebookUri 7) (InL answered) >>= (`shouldBe` InL answered) + + it "leaves a null answer alone" $ + runCodeAction (askedAbout notebookUri 7) (InR Null) >>= (`shouldBe` InR Null) + + -- Shadow line 0 is the `fn main() {` we added, which is in no cell. + it "drops an action whose edit lands on a wrapper line" $ do + let answered = [InR (action (Just (documentChangesEdit notebookShadowUri [InL (TextEdit (lineRange 0) "// hi")])))] + runCodeAction (askedAbout notebookUri 7) (InL answered) >>= (`shouldBe` InL []) + + -- Another module in the shadow crate. There's no cell it could be pointed at. + it "drops an action whose edit names a file other than this notebook's shadow document" $ do + let other = filePathToUri (shadowDir <> "/src/other.rs") + let answered = [InR (action (Just (documentChangesEdit other [InL (TextEdit (lineRange 8) ": f64")])))] + runCodeAction (askedAbout notebookUri 7) (InL answered) >>= (`shouldBe` InL []) + + it "drops an action that creates a file" $ do + let answered = [InR (action (Just (createFileEdit (filePathToUri (shadowDir <> "/src/new.rs")))))] + runCodeAction (askedAbout notebookUri 7) (InL answered) >>= (`shouldBe` InL []) + + describe "codeAction/resolve" $ do + -- No text document comes along with a resolve, so the notebook has to be found from the + -- shadow uri the edit names. + it "puts the resolved edit back on the notebook's line" $ do + let answered = action (Just (documentChangesEdit notebookShadowUri [InL (TextEdit (lineRange 8) ": f64")])) + runResolve answered + >>= (`shouldBe` action (Just (documentChangesEdit notebookUri [InL (TextEdit (lineRange 7) ": f64")]))) + + -- The uri is looked for in documentChanges first and a changes map second, so an edit that + -- only has the latter has to find the notebook too. + it "finds the notebook from a changes map as well" $ do + let answered = action (Just (changesEdit notebookShadowUri [TextEdit (lineRange 8) ": f64"])) + runResolve answered + >>= (`shouldBe` action (Just (changesEdit notebookUri [TextEdit (lineRange 7) ": f64"]))) + + it "leaves an action with no edit alone" $ + runResolve (action Nothing) >>= (`shouldBe` action Nothing) + + it "drops an edit that lands on a wrapper line" $ do + let answered = action (Just (documentChangesEdit notebookShadowUri [InL (TextEdit (lineRange 0) "// hi")])) + runResolve answered >>= (`shouldBe` action Nothing) + + describe "a plain .rs file" $ do + -- Not a notebook, so it takes idTransformerParams and there's no wrapper to account for. + -- Getting a shift wrong here would be invisible otherwise. + it "shifts an edit by nothing at all" $ do + let answered = [InR (action (Just (documentChangesEdit plainShadowUri [InL (TextEdit (lineRange 6) ": f64")])))] + runCodeAction (askedAbout plainUri 6) (InL answered) + >>= (`shouldBe` InL [InR (action (Just (documentChangesEdit plainUri [InL (TextEdit (lineRange 6) ": f64")])))]) + +runCodeAction :: MonadIO m + => MessageParams Method_TextDocumentCodeAction + -> MessageResult Method_TextDocumentCodeAction + -> m (MessageResult Method_TextDocumentCodeAction) +runCodeAction = runTransform SMethod_TextDocumentCodeAction + +runResolve :: MonadIO m => CodeAction -> m CodeAction +runResolve answered = runTransform SMethod_CodeActionResolve answered answered + +runTransform :: MonadIO m => ServerRspMethod meth -> MessageParams meth -> MessageResult meth -> m (MessageResult meth) +runTransform meth params result = liftIO $ do + state <- transformerStateWith [notebookDocumentState cellLines, plainDocumentState plainFileLines] + runNoLoggingT $ runReaderT (transformServerRsp' (rustNotebookConfig shadowDir 0) meth params result) state + +askedAbout :: Uri -> UInt -> MessageParams Method_TextDocumentCodeAction +askedAbout u line = CodeActionParams Nothing Nothing (TextDocumentIdentifier u) (lineRange line) + (CodeActionContext [] Nothing Nothing) + +action :: Maybe WorkspaceEdit -> CodeAction +action e = CodeAction "Insert explicit type `f64`" (Just CodeActionKind_QuickFix) Nothing Nothing Nothing + e Nothing Nothing + +lineRange :: UInt -> Range +lineRange l = Range (Position l 0) (Position l 0) + +documentChangesEdit :: Uri -> [TextEdit |? AnnotatedTextEdit] -> WorkspaceEdit +documentChangesEdit u edits = WorkspaceEdit Nothing + (Just [InL (TextDocumentEdit (OptionalVersionedTextDocumentIdentifier u (InR Null)) edits)]) Nothing + +changesEdit :: Uri -> [TextEdit] -> WorkspaceEdit +changesEdit u edits = WorkspaceEdit (Just (M.singleton u edits)) Nothing Nothing + +createFileEdit :: Uri -> WorkspaceEdit +createFileEdit u = WorkspaceEdit Nothing + (Just [InR (InL (CreateFile Nothing AString u Nothing))]) Nothing + +-- | @let pi = 3.14;@ is notebook line 7, so shadow line 8. The @:dep@ line is there so the +-- composed @StripDirective :> HeadTailTransformer@ is the thing under test, not just the half +-- of it that shifts lines. +cellLines :: [Text] +cellLines = [ + ":dep rand = \"0.8\"" + , "" + , "fn area(r: f64) -> f64 {" + , " 3.14159 * r * r" + , "}" + , "" + , "let r = 2.0;" + , "let pi = 3.14;" + , "println!(\"{} {}\", area(r), pi);" + ] + +-- | @let pi = 3.14;@ is line 6 here, and stays line 6 in the shadow document. +plainFileLines :: [Text] +plainFileLines = [ + "fn area(r: f64) -> f64 {" + , " 3.14159 * r * r" + , "}" + , "" + , "fn main() {" + , " let r = 2.0;" + , " let pi = 3.14;" + , " println!(\"{} {}\", area(r), pi);" + , "}" + ] + +main :: IO () +main = runSandwichWithCommandLineArgs defaultOptions tests diff --git a/test/Test/Formatting.hs b/test/Test/Formatting.hs index 44b7b67..c1aa940 100644 --- a/test/Test/Formatting.hs +++ b/test/Test/Formatting.hs @@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedLists #-} -module Test.Formatting where +module Test.Formatting (tests, main) where import Data.Functor.Identity import qualified Data.Text as T @@ -17,8 +17,8 @@ import Language.LSP.NotebookLanguageServer.Transform.ServerRsp.Hover (mkDocRegex import Language.LSP.NotebookLanguageServer.Transform.Util -spec :: TopSpec -spec = describe "Formatting" $ do +tests :: TopSpec +tests = describe "Formatting" $ do describe "untransformFormattingEdits" $ do it "passes edits through for a plain .rs file" $ do let ds = documentState "/tmp/test.rs" "fn main(){}" @@ -125,4 +125,4 @@ wholeRange doc = Range (Position 0 0) (Position (fromIntegral l) (fromIntegral c where Rope.Position l c = Rope.lengthAsPosition doc main :: IO () -main = runSandwichWithCommandLineArgs defaultOptions spec +main = runSandwichWithCommandLineArgs defaultOptions tests diff --git a/test/Test/Hover.hs b/test/Test/Hover.hs index 9b0d809..b7ca8fb 100644 --- a/test/Test/Hover.hs +++ b/test/Test/Hover.hs @@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedLists #-} -module Test.Hover where +module Test.Hover (tests, main) where import Data.Functor.Identity import Data.String.Interpolate @@ -11,8 +11,8 @@ import Test.Sandwich import Language.LSP.NotebookLanguageServer.Transform.ServerRsp.Hover -spec :: TopSpec -spec = describe "Hover" $ do +tests :: TopSpec +tests = describe "Hover" $ do describe "fixes up document references" $ do let (_projected, transformer :: RustNotebookTransformer, _) = runIdentity $ project transformerParams (listToDoc hoverLines) @@ -38,4 +38,4 @@ hoverLines = [ ] main :: IO () -main = runSandwichWithCommandLineArgs defaultOptions spec +main = runSandwichWithCommandLineArgs defaultOptions tests diff --git a/test/Test/Transformer/HeadTail.hs b/test/Test/Transformer/HeadTail.hs index d91ce55..1865b98 100644 --- a/test/Test/Transformer/HeadTail.hs +++ b/test/Test/Transformer/HeadTail.hs @@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedLists #-} -module Test.Transformer.HeadTail where +module Test.Transformer.HeadTail (tests, main) where import Data.Functor.Identity import Data.String.Interpolate @@ -14,8 +14,8 @@ import TestLib.Core import TestLib.Generators -spec :: TopSpec -spec = describe "HeadTailTransformer" $ do +tests :: TopSpec +tests = describe "HeadTailTransformer" $ do it "works" $ do let (ls, (HeadTailTransformer {}), _) = runIdentity $ project (["fn main() {"], ["}"]) (listToDoc [[i|println!("hi")|]]) ls `shouldBe` (listToDoc [ @@ -44,4 +44,4 @@ format!("Hello {}", "world"); |] main :: IO () -main = runSandwichWithCommandLineArgs defaultOptions spec +main = runSandwichWithCommandLineArgs defaultOptions tests diff --git a/test/Test/Transformer/StripDirective.hs b/test/Test/Transformer/StripDirective.hs index 588e68c..672baed 100644 --- a/test/Test/Transformer/StripDirective.hs +++ b/test/Test/Transformer/StripDirective.hs @@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedLists #-} -module Test.Transformer.StripDirective where +module Test.Transformer.StripDirective (tests, main) where import Data.Functor.Identity import Language.LSP.Notebook.StripDirective @@ -9,8 +9,8 @@ import Language.LSP.Transformer import Test.Sandwich -spec :: TopSpec -spec = describe "StripDirective" $ do +tests :: TopSpec +tests = describe "StripDirective" $ do it "strips out :dep directives" $ do let (ls, ed@(StripDirective _ affectedLines), _) = runIdentity $ project (SDParams True) (listToDoc ["let foo = 42", ":dep rand"]) ls `shouldBe` (listToDoc ["let foo = 42", ""]) @@ -21,4 +21,4 @@ spec = describe "StripDirective" $ do main :: IO () -main = runSandwichWithCommandLineArgs defaultOptions spec +main = runSandwichWithCommandLineArgs defaultOptions tests diff --git a/test/TestLib/Transformers.hs b/test/TestLib/Transformers.hs new file mode 100644 index 0000000..29435c6 --- /dev/null +++ b/test/TestLib/Transformers.hs @@ -0,0 +1,82 @@ +{-# LANGUAGE TypeOperators #-} + +-- | 'DocumentState's holding the real 'RustNotebookTransformer', for the transforms that reach +-- the transformer through 'Control.Monad.Reader.MonadReader' rather than taking it as an +-- argument. +module TestLib.Transformers ( + notebookDocumentState + , plainDocumentState + , transformerStateWith + + , shadowDir + , notebookUri + , notebookShadowUri + , plainUri + , plainShadowUri + ) where + +import Data.Functor.Identity +import qualified Data.Map as M +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.UUID as UUID +import Language.LSP.Notebook +import Language.LSP.NotebookLanguageServer.Transform.ServerRsp.Hover (mkDocRegex) +import Language.LSP.NotebookLanguageServer.Transform.Util +import Language.LSP.Protocol.Types +import Language.LSP.Transformer +import UnliftIO.MVar + + +-- | A notebook's cells, wrapped in @fn main() {@ the way 'transformerParams' says, so every +-- line of the notebook sits one lower in the shadow document. +notebookDocumentState :: [Text] -> DocumentState RustNotebookTransformer +notebookDocumentState = documentStateFor transformerParams notebookUri notebookShadowPath + +-- | A plain @.rs@ file, which takes 'idTransformerParams' and so has no offset at all. +plainDocumentState :: [Text] -> DocumentState RustNotebookTransformer +plainDocumentState = documentStateFor idTransformerParams plainUri plainShadowPath + +documentStateFor :: Params RustNotebookTransformer -> Uri -> FilePath -> [Text] -> DocumentState RustNotebookTransformer +documentStateFor params origUri newPath ls = DocumentState { + transformer = tx + , curLines = docLines + , curLines' = projected + , origUri = origUri + , newUri = filePathToUri newPath + , newPath = newPath + , referenceRegex = mkDocRegex (T.pack newPath) + , documentUuid = UUID.nil + , debouncedDidChange = return () + } + where + docLines = listToDoc ls + (projected, tx, _) = runIdentity $ project params docLines + +-- | A state holding the given open documents, keyed the way the server keys them. +transformerStateWith :: [DocumentState tx] -> IO (TransformerState tx) +transformerStateWith dss = TransformerState + <$> newMVar (M.fromList [(getUri (origUri ds), ds) | ds <- dss]) + <*> newMVar Nothing + <*> newMVar Nothing + +shadowDir :: FilePath +shadowDir = "/home/shadow" + +notebookUri :: Uri +notebookUri = Uri "file:///home/main.ipynb" + +notebookShadowUri :: Uri +notebookShadowUri = filePathToUri notebookShadowPath + +notebookShadowPath :: FilePath +notebookShadowPath = shadowDir <> "/src/notebook.rs" + +plainUri :: Uri +plainUri = Uri "file:///home/plain.rs" + +plainShadowUri :: Uri +plainShadowUri = filePathToUri plainShadowPath + +plainShadowPath :: FilePath +plainShadowPath = shadowDir <> "/src/plain.rs"