diff --git a/hls-plugin-api/hls-plugin-api.cabal b/hls-plugin-api/hls-plugin-api.cabal index 4f59d34a39..dfaff5fb96 100644 --- a/hls-plugin-api/hls-plugin-api.cabal +++ b/hls-plugin-api/hls-plugin-api.cabal @@ -53,6 +53,7 @@ library hs-source-dirs: src build-depends: + , split ^>=0.2.5 , aeson , base >=4.12 && <5 , co-log-core diff --git a/hls-plugin-api/src/Ide/PluginUtils.hs b/hls-plugin-api/src/Ide/PluginUtils.hs index 90e68f85a2..972ee4c3b2 100644 --- a/hls-plugin-api/src/Ide/PluginUtils.hs +++ b/hls-plugin-api/src/Ide/PluginUtils.hs @@ -13,6 +13,7 @@ module Ide.PluginUtils makeDiffTextEditAdditive, diffText, diffText', + diffTextEdit, pluginDescToIdePlugins, idePluginsToPluginDesc, getClientConfig, @@ -45,8 +46,10 @@ import Data.Algorithm.Diff import Data.Algorithm.DiffOutput import Data.Char (isPrint, showLitChar) import Data.Functor (void) +import Data.List.Split import qualified Data.Map as M import qualified Data.Text as T +import Data.Tuple.Extra (both) import Data.Void (Void) import Ide.Plugin.Config import Ide.Plugin.Properties @@ -119,24 +122,28 @@ makeDiffTextEditAdditive :: T.Text -> T.Text -> [TextEdit] makeDiffTextEditAdditive f1 f2 = diffTextEdit f1 f2 SkipDeletions diffTextEdit :: T.Text -> T.Text -> WithDeletions -> [TextEdit] -diffTextEdit fText f2Text withDeletions = r +diffTextEdit fText f2Text withDeletions + = map diffOperationToTextEdit diffOps where - r = map diffOperationToTextEdit diffOps - d = getGroupedDiff (lines $ T.unpack fText) (lines $ T.unpack f2Text) + -- Lossless versions of lines/unlines (they keep the line breaks) + lines = split (dropFinalBlank $ keepDelimsR $ whenElt (== '\n')) + unlines = concat + + (linesL, linesR) = both (lines . T.unpack) (fText, f2Text) diffOps = filter (\x -> (withDeletions == IncludeDeletions) || not (isDeletion x)) - (diffToLineRanges d) - - isDeletion (Deletion _ _) = True - isDeletion _ = False + (diffToLineRanges $ getGroupedDiff linesL linesR) + where + isDeletion (Deletion _ _) = True + isDeletion _ = False diffOperationToTextEdit :: DiffOperation LineRange -> TextEdit diffOperationToTextEdit (Change fm to) = TextEdit range nt where range = calcRange fm - nt = T.pack $ init $ unlines $ lrContents to + nt = T.pack $ unlines $ lrContents to {- In order to replace everything including newline characters, diff --git a/hls-plugin-api/test/Ide/PluginUtilsTest.hs b/hls-plugin-api/test/Ide/PluginUtilsTest.hs index 01f45b4f63..9807c96b33 100644 --- a/hls-plugin-api/test/Ide/PluginUtilsTest.hs +++ b/hls-plugin-api/test/Ide/PluginUtilsTest.hs @@ -20,9 +20,9 @@ import Ide.Plugin.Properties (KeyNamePath (..), usePropertyByPath, usePropertyByPathEither) import qualified Ide.Plugin.RangeMap as RangeMap -import Ide.PluginUtils (extractTextInRange, unescape) +import Ide.PluginUtils (extractTextInRange, unescape, diffTextEdit, WithDeletions (IncludeDeletions)) import Language.LSP.Protocol.Types (Position (..), Range (Range), - UInt, isSubrangeOf) + UInt, isSubrangeOf, TextEdit(TextEdit), mkRange) import Test.Tasty import Test.Tasty.Golden (goldenVsStringDiff) import Test.Tasty.HUnit @@ -31,6 +31,7 @@ import Test.Tasty.QuickCheck tests :: TestTree tests = testGroup "PluginUtils" [ unescapeTest + , diffTextEditTest , extractTextInRangeTest , localOption (QuickCheckMaxSize 10000) $ testProperty "RangeMap-List filtering identical" $ @@ -56,6 +57,56 @@ unescapeTest = testGroup "unescape" unescape "\"\\n\\t\"" @?= "\"\\n\\t\"" ] +diffTextEditTest :: TestTree +diffTextEditTest = testGroup "diffTextEdit" + [ testGroup "inserting line at EOF" + [ testCase "both newline-terminated (linux-style vs linux-style)" + $ diffTextEditComplete "foo\n" + "foo\nbar\n" + @?= [textEdit "bar\n" + (mkRange 1 0 1 0)] + , testCase "neither newline-terminated (win-style vs win-style)" + $ diffTextEditComplete "foo" + "foo\nbar" + @?= [textEdit "foo\nbar" + (mkRange 0 0 0 3)] + , testCase "only left newline-terminated" + $ diffTextEditComplete "foo\n" + "foo\nbar" + @?= [textEdit "bar" + (mkRange 1 0 1 0)] + , testCase "only right newline-terminated" + $ diffTextEditComplete "foo" + "foo\nbar\n" + @?= [textEdit "foo\nbar\n" + (mkRange 0 0 0 3)] + ] + , testGroup "deleting line at EOF" + [ testCase "both newline-terminated (linux-style vs linux-style)" + $ diffTextEditComplete "foo\nbar\n" + "foo\n" + @?= [textEdit "" + (mkRange 1 0 2 0)] + , testCase "neither newline-terminated (win-style vs win-style)" + $ diffTextEditComplete "foo\nbar" + "foo" + @?= [textEdit "foo" + (mkRange 0 0 1 3)] + , testCase "only left newline-terminated" + $ diffTextEditComplete "foo\nbar" + "foo\n" + @?= [textEdit "" + (mkRange 1 0 2 0)] + , testCase "only right newline-terminated" + $ diffTextEditComplete "foo\nbar\n" + "foo" + @?= [textEdit "foo" + (mkRange 0 0 1 4)] + ] + ] + where diffTextEditComplete from to = diffTextEdit from to IncludeDeletions + textEdit = flip TextEdit + extractTextInRangeTest :: TestTree extractTextInRangeTest = testGroup "extractTextInRange" [ testCase "inline range" $ diff --git a/plugins/hls-case-split-plugin/test/Main.hs b/plugins/hls-case-split-plugin/test/Main.hs index f5ccfecb52..a6538706e5 100644 --- a/plugins/hls-case-split-plugin/test/Main.hs +++ b/plugins/hls-case-split-plugin/test/Main.hs @@ -45,8 +45,7 @@ codeActionTests = testGroup Prelude.flip inspectCodeAction [title] -- Windows support - , expectFailBecause "https://github.com/haskell/haskell-language-server/issues/5059" - $ goldenWithClass "Like TNoPatternsNoBraces, but lacks line terminator at EOF" "TNoPatternsNoBracesWindows" $ + , goldenWithClass "Like TNoPatternsNoBraces, but lacks line terminator at EOF" "TNoPatternsNoBracesWindows" $ Prelude.flip inspectCodeAction [title] -- Patterns with irregular indentation diff --git a/plugins/hls-class-plugin/test/Main.hs b/plugins/hls-class-plugin/test/Main.hs index 68d3fa1006..4b9a3c0676 100644 --- a/plugins/hls-class-plugin/test/Main.hs +++ b/plugins/hls-class-plugin/test/Main.hs @@ -46,6 +46,8 @@ codeActionTests = testGroup ] , goldenWithClass "Creates a placeholder for '=='" "T1" "eq" $ getActionByTitle "Add placeholders for '=='" + , goldenWithClass "Like previous one, but this file has no line terminator" "T1W" "eq" $ + getActionByTitle "Add placeholders for '=='" , goldenWithClass "Creates a placeholder for '/='" "T1" "ne" $ getActionByTitle "Add placeholders for '/='" , goldenWithClass "Creates a placeholder for both '==' and '/='" "T1" "all" $ diff --git a/plugins/hls-class-plugin/test/testdata/T1W.eq.expected.hs b/plugins/hls-class-plugin/test/testdata/T1W.eq.expected.hs new file mode 100644 index 0000000000..37fb11727d --- /dev/null +++ b/plugins/hls-class-plugin/test/testdata/T1W.eq.expected.hs @@ -0,0 +1,6 @@ +module T1 where + +data X = X + +instance Eq X where + (==) = _ \ No newline at end of file diff --git a/plugins/hls-class-plugin/test/testdata/T1W.hs b/plugins/hls-class-plugin/test/testdata/T1W.hs new file mode 100644 index 0000000000..f6e5a0fc13 --- /dev/null +++ b/plugins/hls-class-plugin/test/testdata/T1W.hs @@ -0,0 +1,5 @@ +module T1 where + +data X = X + +instance Eq X where \ No newline at end of file