packages feed

hls-plugin-api 1.5.0.0 → 1.6.0.0

raw patch · 8 files changed

+535/−179 lines, 8 filesdep +criteriondep +deepseqdep +hw-fingertreedep −processdep ~basedep ~hls-graphPVP ok

version bump matches the API change (PVP)

Dependencies added: criterion, deepseq, hw-fingertree, megaparsec, random, random-fu, tasty-quickcheck

Dependencies removed: process

Dependency ranges changed: base, hls-graph

API changes (from Hackage documentation)

- Ide.Plugin.Config: instance Data.Aeson.Types.FromJSON.FromJSON Ide.Plugin.Config.CheckParents
- Ide.Plugin.Config: instance Data.Aeson.Types.FromJSON.FromJSON Ide.Plugin.Config.PluginConfig
- Ide.Plugin.Config: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Plugin.Config.CheckParents
- Ide.Plugin.Config: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Plugin.Config.Config
- Ide.Plugin.Config: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Plugin.Config.PluginConfig
- Ide.Plugin.Config: instance Data.Default.Class.Default Ide.Plugin.Config.Config
- Ide.Plugin.Config: instance Data.Default.Class.Default Ide.Plugin.Config.PluginConfig
- Ide.Plugin.Config: instance GHC.Classes.Eq Ide.Plugin.Config.CheckParents
- Ide.Plugin.Config: instance GHC.Classes.Eq Ide.Plugin.Config.Config
- Ide.Plugin.Config: instance GHC.Classes.Eq Ide.Plugin.Config.PluginConfig
- Ide.Plugin.Config: instance GHC.Classes.Ord Ide.Plugin.Config.CheckParents
- Ide.Plugin.Config: instance GHC.Generics.Generic Ide.Plugin.Config.CheckParents
- Ide.Plugin.Config: instance GHC.Show.Show Ide.Plugin.Config.CheckParents
- Ide.Plugin.Config: instance GHC.Show.Show Ide.Plugin.Config.Config
- Ide.Plugin.Config: instance GHC.Show.Show Ide.Plugin.Config.PluginConfig
- Ide.Types: [configEnableGenericConfig] :: ConfigDescriptor -> Bool
+ Ide.Plugin.Config: [cabalFormattingProvider] :: Config -> !Text
+ Ide.Plugin.Config: [plcFoldingRangeOn] :: PluginConfig -> !Bool
+ Ide.Plugin.RangeMap: RangeMap :: IntervalMap Position a -> RangeMap a
+ Ide.Plugin.RangeMap: [unRangeMap] :: RangeMap a -> IntervalMap Position a
+ Ide.Plugin.RangeMap: filterByRange :: Range -> RangeMap a -> [a]
+ Ide.Plugin.RangeMap: fromList :: (a -> Range) -> [a] -> RangeMap a
+ Ide.Plugin.RangeMap: fromList' :: [(Range, a)] -> RangeMap a
+ Ide.Plugin.RangeMap: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Ide.Plugin.RangeMap.RangeMap a)
+ Ide.Plugin.RangeMap: instance Data.Foldable.Foldable Ide.Plugin.RangeMap.RangeMap
+ Ide.Plugin.RangeMap: instance Data.Traversable.Traversable Ide.Plugin.RangeMap.RangeMap
+ Ide.Plugin.RangeMap: instance GHC.Base.Functor Ide.Plugin.RangeMap.RangeMap
+ Ide.Plugin.RangeMap: instance GHC.Base.Monoid (Ide.Plugin.RangeMap.RangeMap a)
+ Ide.Plugin.RangeMap: instance GHC.Base.Semigroup (Ide.Plugin.RangeMap.RangeMap a)
+ Ide.Plugin.RangeMap: newtype RangeMap a
+ Ide.PluginUtils: extendLineStart :: Range -> Range
+ Ide.PluginUtils: extendNextLine :: Range -> Range
+ Ide.PluginUtils: unescape :: Text -> Text
+ Ide.Types: AlwaysCheck :: CheckParents
+ Ide.Types: CheckOnSave :: CheckParents
+ Ide.Types: Config :: CheckParents -> !Bool -> !Text -> !Text -> !Int -> !Map PluginId PluginConfig -> Config
+ Ide.Types: NeverCheck :: CheckParents
+ Ide.Types: PluginConfig :: !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Object -> PluginConfig
+ Ide.Types: [cabalFormattingProvider] :: Config -> !Text
+ Ide.Types: [checkParents] :: Config -> CheckParents
+ Ide.Types: [checkProject] :: Config -> !Bool
+ Ide.Types: [configInitialGenericConfig] :: ConfigDescriptor -> PluginConfig
+ Ide.Types: [formattingProvider] :: Config -> !Text
+ Ide.Types: [maxCompletions] :: Config -> !Int
+ Ide.Types: [plcCallHierarchyOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcCodeActionsOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcCodeLensOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcCompletionOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcConfig] :: PluginConfig -> !Object
+ Ide.Types: [plcDiagnosticsOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcFoldingRangeOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcGlobalOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcHoverOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcRenameOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcSelectionRangeOn] :: PluginConfig -> !Bool
+ Ide.Types: [plcSymbolsOn] :: PluginConfig -> !Bool
+ Ide.Types: [plugins] :: Config -> !Map PluginId PluginConfig
+ Ide.Types: data CheckParents
+ Ide.Types: data Config
+ Ide.Types: data PluginConfig
+ Ide.Types: instance Data.Aeson.Types.FromJSON.FromJSON Ide.Types.CheckParents
+ Ide.Types: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Types.CheckParents
+ Ide.Types: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Types.Config
+ Ide.Types: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Types.PluginConfig
+ Ide.Types: instance Data.Default.Class.Default Ide.Types.Config
+ Ide.Types: instance Data.Default.Class.Default Ide.Types.PluginConfig
+ Ide.Types: instance GHC.Classes.Eq Ide.Types.CheckParents
+ Ide.Types: instance GHC.Classes.Eq Ide.Types.Config
+ Ide.Types: instance GHC.Classes.Eq Ide.Types.PluginConfig
+ Ide.Types: instance GHC.Classes.Ord Ide.Types.CheckParents
+ Ide.Types: instance GHC.Generics.Generic Ide.Types.CheckParents
+ Ide.Types: instance GHC.Show.Show Ide.Types.CheckParents
+ Ide.Types: instance GHC.Show.Show Ide.Types.Config
+ Ide.Types: instance GHC.Show.Show Ide.Types.PluginConfig
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.Completion.CompletionItem
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.CompletionItemResolve
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentFoldingRange
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.CompletionItemResolve
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentFoldingRange
- Ide.Plugin.Config: Config :: CheckParents -> !Bool -> !Text -> !Int -> !Map Text PluginConfig -> Config
+ Ide.Plugin.Config: Config :: CheckParents -> !Bool -> !Text -> !Text -> !Int -> !Map PluginId PluginConfig -> Config
- Ide.Plugin.Config: PluginConfig :: !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Object -> PluginConfig
+ Ide.Plugin.Config: PluginConfig :: !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Object -> PluginConfig
- Ide.Plugin.Config: [plugins] :: Config -> !Map Text PluginConfig
+ Ide.Plugin.Config: [plugins] :: Config -> !Map PluginId PluginConfig
- Ide.Plugin.Config: getConfigFromNotification :: Config -> Value -> Either Text Config
+ Ide.Plugin.Config: getConfigFromNotification :: IdePlugins s -> Config -> Value -> Either Text Config
- Ide.Plugin.Config: parseConfig :: Config -> Value -> Parser Config
+ Ide.Plugin.Config: parseConfig :: IdePlugins s -> Config -> Value -> Parser Config
- Ide.PluginUtils: configForPlugin :: Config -> PluginId -> PluginConfig
+ Ide.PluginUtils: configForPlugin :: Config -> PluginDescriptor c -> PluginConfig
- Ide.PluginUtils: getPluginConfig :: MonadLsp Config m => PluginId -> m PluginConfig
+ Ide.PluginUtils: getPluginConfig :: MonadLsp Config m => PluginDescriptor c -> m PluginConfig
- Ide.PluginUtils: usePropertyLsp :: (HasProperty s k t r, MonadLsp Config m) => KeyNameProxy s -> PluginId -> Properties r -> m (ToHsType t)
+ Ide.PluginUtils: usePropertyLsp :: (HasProperty s k t r, MonadLsp Config m) => KeyNameProxy s -> PluginDescriptor c -> Properties r -> m (ToHsType t)
- Ide.Types: ConfigDescriptor :: Bool -> Bool -> CustomConfig -> ConfigDescriptor
+ Ide.Types: ConfigDescriptor :: PluginConfig -> Bool -> CustomConfig -> ConfigDescriptor
- Ide.Types: configForPlugin :: Config -> PluginId -> PluginConfig
+ Ide.Types: configForPlugin :: Config -> PluginDescriptor c -> PluginConfig
- Ide.Types: pluginEnabledConfig :: (PluginConfig -> Bool) -> PluginId -> Config -> Bool
+ Ide.Types: pluginEnabledConfig :: (PluginConfig -> Bool) -> PluginConfig -> Bool

Files

+ bench/Main.hs view
@@ -0,0 +1,56 @@+-- A benchmark comparing the performance characteristics of list-based+-- vs RangeMap-based "in-range filtering" approaches+module Main (main) where++import           Control.DeepSeq        (force)+import           Control.Exception      (evaluate)+import           Control.Monad          (replicateM)+import qualified Criterion+import qualified Criterion.Main+import           Data.Random            (RVar)+import qualified Data.Random            as Fu+import qualified Ide.Plugin.RangeMap    as RangeMap+import           Language.LSP.Types     (Position (..), Range (..), UInt,+                                         isSubrangeOf)+import qualified System.Random.Stateful as Random+++genRangeList :: Int -> RVar [Range]+genRangeList n = replicateM n genRange++genRange :: RVar Range+genRange = do+  x1 <- genPosition+  delta <- genRangeLength+  let x2 = x1 { _character = _character x1 + delta }+  pure $ Range x1 x2+  where+    genRangeLength :: RVar UInt+    genRangeLength = fromInteger <$> Fu.uniform 5 50++genPosition :: RVar Position+genPosition = Position+  <$> (fromInteger <$> Fu.uniform 0 10000)+  <*> (fromInteger <$> Fu.uniform 0 150)++filterRangeList :: Range -> [Range] -> [Range]+filterRangeList r = filter (isSubrangeOf r)++main :: IO ()+main = do+  rangeLists@[rangeList100, rangeList1000, rangeList10000]+    <- traverse (Fu.sampleFrom Random.globalStdGen . genRangeList) [100, 1000, 10000]+  [rangeMap100, rangeMap1000, rangeMap10000] <- evaluate $ force $ map (RangeMap.fromList id) rangeLists+  targetRange <- Fu.sampleFrom Random.globalStdGen genRange+  Criterion.Main.defaultMain+    [ Criterion.bgroup "List"+        [ Criterion.bench "Size 100" $ Criterion.nf (filterRangeList targetRange) rangeList100+        , Criterion.bench "Size 1000" $ Criterion.nf (filterRangeList targetRange) rangeList1000+        , Criterion.bench "Size 10000" $ Criterion.nf (filterRangeList targetRange) rangeList10000+        ]+    , Criterion.bgroup "RangeMap"+        [ Criterion.bench "Size 100" $ Criterion.nf (RangeMap.filterByRange targetRange) rangeMap100+        , Criterion.bench "Size 1000" $ Criterion.nf (RangeMap.filterByRange targetRange) rangeMap1000+        , Criterion.bench "Size 10000" $ Criterion.nf (RangeMap.filterByRange targetRange) rangeMap10000+        ]+    ]
hls-plugin-api.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name:          hls-plugin-api-version:       1.5.0.0+version:       1.6.0.0 synopsis:      Haskell Language Server API for plugin communication description:   Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>@@ -20,6 +20,14 @@   default:     False   manual:      True +-- This flag can be used to avoid the dependency on hw-fingertree.+-- We can set this temporarily if we have problems building hw-fingertree+-- for a new version of GHC.+flag use-fingertree+  description: Use fingertree implementation of RangeMap+  default:     True+  manual:      False+ source-repository head   type:     git   location: https://github.com/haskell/haskell-language-server@@ -29,6 +37,7 @@     Ide.Plugin.Config     Ide.Plugin.ConfigUtils     Ide.Plugin.Properties+    Ide.Plugin.RangeMap     Ide.PluginUtils     Ide.Types @@ -46,17 +55,17 @@     , filepath     , ghc     , hashable-    , hls-graph             ^>= 1.8+    , hls-graph             ^>= 1.9     , lens     , lens-aeson     , lsp                   ^>=1.6.0.0     , opentelemetry         >=0.4     , optparse-applicative-    , process     , regex-tdfa            >=1.3.1.0     , text     , transformers     , unordered-containers+    , megaparsec > 9    if os(windows)     build-depends: Win32@@ -70,7 +79,13 @@    if flag(pedantic)     ghc-options: -Werror+  if impl(ghc >= 9)+    ghc-options: -Wunused-packages +  if flag(use-fingertree)+    cpp-options: -DUSE_FINGERTREE+    build-depends: hw-fingertree+   default-language:   Haskell2010   default-extensions:     DataKinds@@ -90,4 +105,26 @@     , tasty     , tasty-hunit     , tasty-rerun+    , tasty-quickcheck+    , text     , lsp-types+    , containers++benchmark rangemap-benchmark+  -- Benchmark doesn't make sense if fingertree implementation+  -- is not used.+  if !flag(use-fingertree)+    buildable: False+  type:             exitcode-stdio-1.0+  default-language: Haskell2010+  hs-source-dirs:   bench+  main-is:          Main.hs+  ghc-options:      -threaded -Wall+  build-depends:+      base+    , hls-plugin-api+    , lsp-types+    , criterion+    , random+    , random-fu+    , deepseq
src/Ide/Plugin/Config.hs view
@@ -1,10 +1,8 @@-{-# LANGUAGE DeriveAnyClass     #-}-{-# LANGUAGE DeriveGeneric      #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleInstances  #-} {-# LANGUAGE OverloadedStrings  #-}-{-# LANGUAGE RecordWildCards    #-} {-# LANGUAGE TypeFamilies       #-}+{-# LANGUAGE ViewPatterns       #-} module Ide.Plugin.Config     ( getConfigFromNotification     , Config(..)@@ -14,61 +12,32 @@     ) where  import           Control.Applicative+import           Control.Lens        (preview) import           Data.Aeson          hiding (Error) import qualified Data.Aeson          as A+import           Data.Aeson.Lens     (_String) import qualified Data.Aeson.Types    as A import           Data.Default-import qualified Data.Map            as Map+import qualified Data.Map.Strict     as Map+import           Data.Maybe          (fromMaybe) import qualified Data.Text           as T-import           GHC.Generics        (Generic)+import           GHC.Exts            (toList)+import           Ide.Types  -- ---------------------------------------------------------------------  -- | Given a DidChangeConfigurationNotification message, this function returns the parsed -- Config object if possible.-getConfigFromNotification :: Config -> A.Value -> Either T.Text Config-getConfigFromNotification defaultValue p =-  case A.parse (parseConfig defaultValue) p of+getConfigFromNotification :: IdePlugins s -> Config -> A.Value -> Either T.Text Config+getConfigFromNotification plugins defaultValue p =+  case A.parse (parseConfig plugins defaultValue) p of     A.Success c -> Right c     A.Error err -> Left $ T.pack err  -- ----------------------------------------------------------------------data CheckParents-    -- Note that ordering of constructors is meaningful and must be monotonically-    -- increasing in the scenarios where parents are checked-    = NeverCheck-    | CheckOnSave-    | AlwaysCheck-  deriving stock (Eq, Ord, Show, Generic)-  deriving anyclass (FromJSON, ToJSON) --- | We (initially anyway) mirror the hie configuration, so that existing--- clients can simply switch executable and not have any nasty surprises.  There--- will be surprises relating to config options being ignored, initially though.-data Config =-  Config-    { checkParents       :: CheckParents-    , checkProject       :: !Bool-    , formattingProvider :: !T.Text-    , maxCompletions     :: !Int-    , plugins            :: !(Map.Map T.Text PluginConfig)-    } deriving (Show,Eq)--instance Default Config where-  def = Config-    { checkParents                = CheckOnSave-    , checkProject                = True-    -- , formattingProvider          = "brittany"-    , formattingProvider          = "ormolu"-    -- , formattingProvider          = "floskell"-    -- , formattingProvider          = "stylish-haskell"-    , maxCompletions              = 40-    , plugins                     = Map.empty-    }---- TODO: Add API for plugins to expose their own LSP config options-parseConfig :: Config -> Value -> A.Parser Config-parseConfig defValue = A.withObject "Config" $ \v -> do+parseConfig :: IdePlugins s -> Config -> Value -> A.Parser Config+parseConfig idePlugins defValue = A.withObject "Config" $ \v -> do     -- Officially, we use "haskell" as the section name but for     -- backwards compatibility we also accept "languageServerHaskell"     c <- v .: "haskell" <|> v .:? "languageServerHaskell"@@ -78,74 +47,29 @@         <$> (o .:? "checkParents" <|> v .:? "checkParents") .!= checkParents defValue         <*> (o .:? "checkProject" <|> v .:? "checkProject") .!= checkProject defValue         <*> o .:? "formattingProvider"                      .!= formattingProvider defValue+        <*> o .:? "cabalFormattingProvider"                 .!= cabalFormattingProvider defValue         <*> o .:? "maxCompletions"                          .!= maxCompletions defValue-        <*> o .:? "plugin"                                  .!= plugins defValue+        <*> A.explicitParseFieldMaybe (parsePlugins idePlugins) o "plugin" .!= plugins defValue -instance A.ToJSON Config where-  toJSON Config{..} =-      object [ "haskell" .= r ]-    where-      r = object [ "checkParents"                .= checkParents-                 , "checkProject"                .= checkProject-                 , "formattingProvider"          .= formattingProvider-                 , "maxCompletions"              .= maxCompletions-                 , "plugin"                      .= plugins-                 ]+-- | Parse the 'PluginConfig'.+--   Since we need to fall back to default values if we do not find one in the input,+--   we need the map of plugin-provided defaults, as in 'parseConfig'.+parsePlugins :: IdePlugins s -> Value -> A.Parser (Map.Map PluginId PluginConfig)+parsePlugins (IdePlugins plugins) = A.withObject "Config.plugins" $ \o -> do+  let -- parseOne :: Key -> Value -> A.Parser (T.Text, PluginConfig)+      parseOne (fmap PluginId . preview _String . toJSON -> Just pId) pConfig = do+        let defPluginConfig = fromMaybe def $ lookup pId defValue+        pConfig' <- parsePluginConfig defPluginConfig pConfig+        return (pId, pConfig')+      parseOne _ _ = fail "Expected plugin id to be a string"+      defValue = map (\p -> (pluginId p, configInitialGenericConfig (pluginConfigDescriptor p))) plugins+  plugins <- mapM (uncurry parseOne) (toList o)+  return $ Map.fromList plugins  -- --------------------------------------------------------------------- --- | A PluginConfig is a generic configuration for a given HLS plugin.  It--- provides a "big switch" to turn it on or off as a whole, as well as small--- switches per feature, and a slot for custom config.--- This provides a regular naming scheme for all plugin config.-data PluginConfig =-    PluginConfig-      { plcGlobalOn         :: !Bool-      , plcCallHierarchyOn  :: !Bool-      , plcCodeActionsOn    :: !Bool-      , plcCodeLensOn       :: !Bool-      , plcDiagnosticsOn    :: !Bool-      , plcHoverOn          :: !Bool-      , plcSymbolsOn        :: !Bool-      , plcCompletionOn     :: !Bool-      , plcRenameOn         :: !Bool-      , plcSelectionRangeOn :: !Bool-      , plcConfig           :: !A.Object-      } deriving (Show,Eq)--instance Default PluginConfig where-  def = PluginConfig-      { plcGlobalOn         = True-      , plcCallHierarchyOn  = True-      , plcCodeActionsOn    = True-      , plcCodeLensOn       = True-      , plcDiagnosticsOn    = True-      , plcHoverOn          = True-      , plcSymbolsOn        = True-      , plcCompletionOn     = True-      , plcRenameOn         = True-      , plcSelectionRangeOn = True-      , plcConfig           = mempty-      }--instance A.ToJSON PluginConfig where-    toJSON (PluginConfig g ch ca cl d h s c rn sr cfg) = r-      where-        r = object [ "globalOn"         .= g-                   , "callHierarchyOn"  .= ch-                   , "codeActionsOn"    .= ca-                   , "codeLensOn"       .= cl-                   , "diagnosticsOn"    .= d-                   , "hoverOn"          .= h-                   , "symbolsOn"        .= s-                   , "completionOn"     .= c-                   , "renameOn"         .= rn-                   , "selectionRangeOn" .= sr-                   , "config"           .= cfg-                   ]--instance A.FromJSON PluginConfig where-  parseJSON = A.withObject "PluginConfig" $ \o  -> PluginConfig+parsePluginConfig :: PluginConfig -> Value -> A.Parser PluginConfig+parsePluginConfig def = A.withObject "PluginConfig" $ \o  -> PluginConfig       <$> o .:? "globalOn"         .!= plcGlobalOn def       <*> o .:? "callHierarchyOn"  .!= plcCallHierarchyOn def       <*> o .:? "codeActionsOn"    .!= plcCodeActionsOn def@@ -156,6 +80,7 @@       <*> o .:? "completionOn"     .!= plcCompletionOn  def       <*> o .:? "renameOn"         .!= plcRenameOn      def       <*> o .:? "selectionRangeOn" .!= plcSelectionRangeOn def+      <*> o .:? "foldingRangeOn" .!= plcFoldingRangeOn def       <*> o .:? "config"           .!= plcConfig        def  -- ---------------------------------------------------------------------
src/Ide/Plugin/ConfigUtils.hs view
@@ -9,10 +9,10 @@ import qualified Data.Aeson            as A import           Data.Aeson.Lens       (_Object) import qualified Data.Aeson.Types      as A-import           Data.Default          (def)+import           Data.Default import qualified Data.Dependent.Map    as DMap import qualified Data.Dependent.Sum    as DSum-import           Data.List             (nub)+import           Data.List.Extra       (nubOrd) import           Data.String           (IsString (fromString)) import qualified Data.Text             as T import           Ide.Plugin.Config@@ -62,12 +62,14 @@         -- }         --         genericDefaultConfig =-          let x = ["diagnosticsOn" A..= True | configHasDiagnostics] <> nub (mconcat (handlersToGenericDefaultConfig <$> handlers))-           in case x of-                -- if the plugin has only one capability, we produce globalOn instead of the specific one;-                -- otherwise we don't produce globalOn at all-                [_] -> ["globalOn" A..= True]-                _   -> x+            let x = ["diagnosticsOn" A..= True | configHasDiagnostics]+                        <> nubOrd (mconcat+                            (handlersToGenericDefaultConfig configInitialGenericConfig <$> handlers))+            in case x of+                    -- if the plugin has only one capability, we produce globalOn instead of the specific one;+                    -- otherwise we don't produce globalOn at all+                    [_] -> ["globalOn" A..= plcGlobalOn configInitialGenericConfig]+                    _   -> x         -- Example:         --         -- {@@ -82,15 +84,15 @@         (PluginId pId) = pluginId          -- This function captures ide methods registered by the plugin, and then converts it to kv pairs-        handlersToGenericDefaultConfig :: DSum.DSum IdeMethod f -> [A.Pair]-        handlersToGenericDefaultConfig (IdeMethod m DSum.:=> _) = case m of-          STextDocumentCodeAction           -> ["codeActionsOn" A..= True]-          STextDocumentCodeLens             -> ["codeLensOn" A..= True]-          STextDocumentRename               -> ["renameOn" A..= True]-          STextDocumentHover                -> ["hoverOn" A..= True]-          STextDocumentDocumentSymbol       -> ["symbolsOn" A..= True]-          STextDocumentCompletion           -> ["completionOn" A..= True]-          STextDocumentPrepareCallHierarchy -> ["callHierarchyOn" A..= True]+        handlersToGenericDefaultConfig :: PluginConfig -> DSum.DSum IdeMethod f -> [A.Pair]+        handlersToGenericDefaultConfig PluginConfig{..} (IdeMethod m DSum.:=> _) = case m of+          STextDocumentCodeAction           -> ["codeActionsOn" A..= plcCodeActionsOn]+          STextDocumentCodeLens             -> ["codeLensOn" A..= plcCodeLensOn]+          STextDocumentRename               -> ["renameOn" A..= plcRenameOn]+          STextDocumentHover                -> ["hoverOn" A..= plcHoverOn]+          STextDocumentDocumentSymbol       -> ["symbolsOn" A..= plcSymbolsOn]+          STextDocumentCompletion           -> ["completionOn" A..= plcCompletionOn]+          STextDocumentPrepareCallHierarchy -> ["callHierarchyOn" A..= plcCallHierarchyOn]           _                                 -> []  -- | Generates json schema used in haskell vscode extension@@ -106,7 +108,7 @@         genericSchema =           let x =                 [toKey' "diagnosticsOn" A..= schemaEntry "diagnostics" | configHasDiagnostics]-                  <> nub (mconcat (handlersToGenericSchema <$> handlers))+                  <> nubOrd (mconcat (handlersToGenericSchema <$> handlers))            in case x of                 -- If the plugin has only one capability, we produce globalOn instead of the specific one;                 -- otherwise we don't produce globalOn at all
+ src/Ide/Plugin/RangeMap.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE DeriveFoldable             #-}+{-# LANGUAGE DeriveFunctor              #-}+{-# LANGUAGE DeriveTraversable          #-}+{-# LANGUAGE DerivingStrategies         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- | A map that allows fast \"in-range\" filtering. 'RangeMap' is meant+-- to be constructed once and cached as part of a Shake rule. If+-- not, the map will be rebuilt upon each invocation, yielding slower+-- results compared to the list-based approach!+--+-- Note that 'RangeMap' falls back to the list-based approach if+-- `use-fingertree` flag of `hls-plugin-api` is set to false.+module Ide.Plugin.RangeMap+  ( RangeMap(..),+    fromList,+    fromList',+    filterByRange,+  ) where++import           Data.Bifunctor                           (first)+import           Data.Foldable                            (foldl')+import           Development.IDE.Graph.Classes            (NFData)+import           Language.LSP.Types                       (Position,+                                                           Range (Range),+                                                           isSubrangeOf)+#ifdef USE_FINGERTREE+import qualified HaskellWorks.Data.IntervalMap.FingerTree as IM+#endif++-- | A map from code ranges to values.+#ifdef USE_FINGERTREE+newtype RangeMap a = RangeMap+  { unRangeMap :: IM.IntervalMap Position a+    -- ^ 'IM.Interval' of 'Position' corresponds to a 'Range'+  }+  deriving newtype (NFData, Semigroup, Monoid)+  deriving stock (Functor, Foldable, Traversable)+#else+newtype RangeMap a = RangeMap+  { unRangeMap :: [(Range, a)] }+  deriving newtype (NFData, Semigroup, Monoid)+  deriving stock (Functor, Foldable, Traversable)+#endif++-- | Construct a 'RangeMap' from a 'Range' accessor and a list of values.+fromList :: (a -> Range) -> [a] -> RangeMap a+fromList extractRange = fromList' . map (\x -> (extractRange x, x))++fromList' :: [(Range, a)] -> RangeMap a+#ifdef USE_FINGERTREE+fromList' = RangeMap . toIntervalMap . map (first rangeToInterval)+  where+    toIntervalMap :: Ord v => [(IM.Interval v, a)] -> IM.IntervalMap v a+    toIntervalMap = foldl' (\m (i, v) -> IM.insert i v m) IM.empty+#else+fromList' = RangeMap+#endif++-- | Filter a 'RangeMap' by a given 'Range'.+filterByRange :: Range -> RangeMap a -> [a]+#ifdef USE_FINGERTREE+filterByRange range = map snd . IM.dominators (rangeToInterval range) . unRangeMap+#else+filterByRange range = map snd . filter (isSubrangeOf range . fst) . unRangeMap+#endif++#ifdef USE_FINGERTREE+-- NOTE(ozkutuk): In itself, this conversion is wrong. As Michael put it:+-- "LSP Ranges have exclusive upper bounds, whereas the intervals here are+-- supposed to be closed (i.e. inclusive at both ends)"+-- However, in our use-case this turns out not to be an issue (supported+-- by the accompanying property test).  I think the reason for this is,+-- even if rangeToInterval isn't a correct 1:1 conversion by itself, it+-- is used for both the construction of the RangeMap and during the actual+-- filtering (filterByRange), so it still behaves identical to the list+-- approach.+-- This definition isn't exported from the module, therefore we need not+-- worry about other uses where it potentially makes a difference.+rangeToInterval :: Range -> IM.Interval Position+rangeToInterval (Range s e) = IM.Interval s e+#endif
src/Ide/PluginUtils.hs view
@@ -2,9 +2,12 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies      #-} module Ide.PluginUtils-  ( WithDeletions(..),-    getProcessID,+  ( -- * LSP Range manipulation functions     normalize,+    extendNextLine,+    extendLineStart,+    WithDeletions(..),+    getProcessID,     makeDiffTextEdit,     makeDiffTextEditAdditive,     diffText,@@ -32,6 +35,7 @@     handleMaybe,     handleMaybeM,     throwPluginError,+    unescape,     ) where @@ -43,10 +47,12 @@ import           Data.Algorithm.Diff import           Data.Algorithm.DiffOutput import           Data.Bifunctor                  (Bifunctor (first))+import           Data.Char                       (isPrint, showLitChar)+import           Data.Functor                    (void) import qualified Data.HashMap.Strict             as H-import           Data.List                       (find) import           Data.String                     (IsString (fromString)) import qualified Data.Text                       as T+import           Data.Void                       (Void) import           Ide.Plugin.Config import           Ide.Plugin.Properties import           Ide.Types@@ -57,14 +63,35 @@                                                   SemanticTokensEdit (_start)) import qualified Language.LSP.Types              as J import           Language.LSP.Types.Capabilities+import qualified Text.Megaparsec                 as P+import qualified Text.Megaparsec.Char            as P+import qualified Text.Megaparsec.Char.Lexer      as P  -- ---------------------------------------------------------------------  -- | Extend to the line below and above to replace newline character.+--+-- >>> normalize (Range (Position 5 5) (Position 5 10))+-- Range (Position 5 0) (Position 6 0) normalize :: Range -> Range-normalize (Range (Position sl _) (Position el _)) =-  Range (Position sl 0) (Position (el + 1) 0)+normalize = extendLineStart . extendNextLine +-- | Extend 'Range' to the start of the next line.+--+-- >>> extendNextLine (Range (Position 5 5) (Position 5 10))+-- Range (Position 5 5) (Position 6 0)+extendNextLine :: Range -> Range+extendNextLine (Range s (Position el _)) =+  Range s (Position (el + 1) 0)++-- | Extend 'Range' to the start of the current line.+--+-- >>> extendLineStart (Range (Position 5 5) (Position 5 10))+-- Range (Position 5 0) (Position 5 10)+extendLineStart :: Range -> Range+extendLineStart (Range (Position sl _) e) =+  Range (Position sl 0) e+ -- ---------------------------------------------------------------------  data WithDeletions = IncludeDeletions | SkipDeletions@@ -167,7 +194,7 @@  -- --------------------------------------------------------------------- -- | Returns the current client configuration. It is not wise to permanently--- cache the returned value of this function, as clients can at runitime change+-- cache the returned value of this function, as clients can at runtime change -- their configuration. -- getClientConfig :: MonadLsp Config m => m Config@@ -178,7 +205,7 @@ -- | Returns the current plugin configuration. It is not wise to permanently -- cache the returned value of this function, as clients can change their -- configuration at runtime.-getPluginConfig :: MonadLsp Config m => PluginId -> m PluginConfig+getPluginConfig :: MonadLsp Config m => PluginDescriptor c -> m PluginConfig getPluginConfig plugin = do     config <- getClientConfig     return $ configForPlugin config plugin@@ -189,7 +216,7 @@ usePropertyLsp ::   (HasProperty s k t r, MonadLsp Config m) =>   KeyNameProxy s ->-  PluginId ->+  PluginDescriptor c ->   Properties r ->   m (ToHsType t) usePropertyLsp kn pId p = do@@ -255,3 +282,34 @@ pluginResponse =   fmap (first (\msg -> ResponseError InternalError (fromString msg) Nothing))     . runExceptT++-- ---------------------------------------------------------------------++type TextParser = P.Parsec Void T.Text++-- | Unescape printable escape sequences within double quotes.+-- This is useful if you have to call 'show' indirectly, and it escapes some characters which you would prefer to+-- display as is.+unescape :: T.Text -> T.Text+unescape input =+    case P.runParser escapedTextParser "inline" input of+        Left _     -> input+        Right strs -> T.pack strs++-- | Parser for a string that contains double quotes. Returns unescaped string.+escapedTextParser :: TextParser String+escapedTextParser = concat <$> P.many (outsideStringLiteral P.<|> stringLiteral)+  where+    outsideStringLiteral :: TextParser String+    outsideStringLiteral = P.someTill (P.anySingleBut '"') (P.lookAhead (void (P.char '"') P.<|> P.eof))++    stringLiteral :: TextParser String+    stringLiteral = do+        inside <- P.char '"' >> P.manyTill P.charLiteral (P.char '"')+        let f '"' = "\\\"" -- double quote should still be escaped+            -- Despite the docs, 'showLitChar' and 'showLitString' from 'Data.Char' DOES ESCAPE unicode printable+            -- characters. So we need to call 'isPrint' from 'Data.Char' manually.+            f ch  = if isPrint ch then [ch] else showLitChar ch ""+            inside' = concatMap f inside++        pure $ "\"" <> inside' <> "\""
src/Ide/Types.hs view
@@ -28,6 +28,7 @@ , IdeNotification(..) , IdePlugins(IdePlugins, ipMap) , DynFlagsModifications(..)+, Config(..), PluginConfig(..), CheckParents(..) , ConfigDescriptor(..), defaultConfigDescriptor, configForPlugin, pluginEnabledConfig , CustomConfig(..), mkCustomConfig , FallbackCodeActionParams(..)@@ -56,10 +57,11 @@ import qualified System.Posix.Process            as P (getProcessID) import           System.Posix.Signals #endif+import           Control.Applicative             ((<|>)) import           Control.Arrow                   ((&&&))-import           Control.Lens                    ((^.))+import           Control.Lens                    ((^.), (.~)) import           Data.Aeson                      hiding (defaultOptions)-import qualified Data.Default+import           Data.Default import           Data.Dependent.Map              (DMap) import qualified Data.Dependent.Map              as DMap import qualified Data.DList                      as DList@@ -67,7 +69,7 @@ import           Data.Hashable                   (Hashable) import           Data.HashMap.Strict             (HashMap) import qualified Data.HashMap.Strict             as HashMap-import           Data.List.Extra                 (sortOn, find)+import           Data.List.Extra                 (find, sortOn) import           Data.List.NonEmpty              (NonEmpty (..), toList) import qualified Data.Map                        as Map import           Data.Maybe@@ -79,7 +81,6 @@ import           Development.IDE.Graph import           GHC                             (DynFlags) import           GHC.Generics-import           Ide.Plugin.Config import           Ide.Plugin.Properties import           Language.LSP.Server             (LspM, getVirtualFile) import           Language.LSP.Types              hiding@@ -88,6 +89,7 @@                                                   SemanticTokensEdit (_start)) import           Language.LSP.Types.Capabilities (ClientCapabilities (ClientCapabilities),                                                   TextDocumentClientCapabilities (_codeAction, _documentSymbol))+import qualified Language.LSP.Types.Lens         as J import           Language.LSP.Types.Lens         as J (HasChildren (children),                                                        HasCommand (command),                                                        HasContents (contents),@@ -107,12 +109,11 @@ import           System.FilePath import           System.IO.Unsafe import           Text.Regex.TDFA.Text            ()-import           Control.Applicative             ((<|>))  -- ---------------------------------------------------------------------  data IdePlugins ideState = IdePlugins_-  { ipMap_ :: HashMap PluginId (PluginDescriptor ideState)+  { ipMap_                :: HashMap PluginId (PluginDescriptor ideState)   , lookupCommandProvider :: CommandId -> Maybe PluginId   } @@ -167,6 +168,107 @@  -- --------------------------------------------------------------------- +-- | We (initially anyway) mirror the hie configuration, so that existing+-- clients can simply switch executable and not have any nasty surprises.  There+-- will be surprises relating to config options being ignored, initially though.+data Config =+  Config+    { checkParents            :: CheckParents+    , checkProject            :: !Bool+    , formattingProvider      :: !T.Text+    , cabalFormattingProvider :: !T.Text+    , maxCompletions          :: !Int+    , plugins                 :: !(Map.Map PluginId PluginConfig)+    } deriving (Show,Eq)++instance ToJSON Config where+  toJSON Config{..} =+      object [ "haskell" .= r ]+    where+      r = object [ "checkParents"                .= checkParents+                 , "checkProject"                .= checkProject+                 , "formattingProvider"          .= formattingProvider+                 , "maxCompletions"              .= maxCompletions+                 , "plugin"                      .= Map.mapKeysMonotonic (\(PluginId p) -> p) plugins+                 ]++instance Default Config where+  def = Config+    { checkParents                = CheckOnSave+    , checkProject                = True+    -- , formattingProvider          = "brittany"+    , formattingProvider          = "ormolu"+    -- , formattingProvider          = "floskell"+    -- , formattingProvider          = "stylish-haskell"+    , cabalFormattingProvider     = "cabal-fmt"+    , maxCompletions              = 40+    , plugins                     = mempty+    }++data CheckParents+    -- Note that ordering of constructors is meaningful and must be monotonically+    -- increasing in the scenarios where parents are checked+    = NeverCheck+    | CheckOnSave+    | AlwaysCheck+  deriving stock (Eq, Ord, Show, Generic)+  deriving anyclass (FromJSON, ToJSON)++-- | A PluginConfig is a generic configuration for a given HLS plugin.  It+-- provides a "big switch" to turn it on or off as a whole, as well as small+-- switches per feature, and a slot for custom config.+-- This provides a regular naming scheme for all plugin config.+data PluginConfig =+    PluginConfig+      { plcGlobalOn         :: !Bool+      , plcCallHierarchyOn  :: !Bool+      , plcCodeActionsOn    :: !Bool+      , plcCodeLensOn       :: !Bool+      , plcDiagnosticsOn    :: !Bool+      , plcHoverOn          :: !Bool+      , plcSymbolsOn        :: !Bool+      , plcCompletionOn     :: !Bool+      , plcRenameOn         :: !Bool+      , plcSelectionRangeOn :: !Bool+      , plcFoldingRangeOn   :: !Bool+      , plcConfig           :: !Object+      } deriving (Show,Eq)++instance Default PluginConfig where+  def = PluginConfig+      { plcGlobalOn         = True+      , plcCallHierarchyOn  = True+      , plcCodeActionsOn    = True+      , plcCodeLensOn       = True+      , plcDiagnosticsOn    = True+      , plcHoverOn          = True+      , plcSymbolsOn        = True+      , plcCompletionOn     = True+      , plcRenameOn         = True+      , plcSelectionRangeOn = True+      , plcFoldingRangeOn = True+      , plcConfig           = mempty+      }++instance ToJSON PluginConfig where+    toJSON (PluginConfig g ch ca cl d h s c rn sr fr cfg) = r+      where+        r = object [ "globalOn"         .= g+                   , "callHierarchyOn"  .= ch+                   , "codeActionsOn"    .= ca+                   , "codeLensOn"       .= cl+                   , "diagnosticsOn"    .= d+                   , "hoverOn"          .= h+                   , "symbolsOn"        .= s+                   , "completionOn"     .= c+                   , "renameOn"         .= rn+                   , "selectionRangeOn" .= sr+                   , "foldingRangeOn"   .= fr+                   , "config"           .= cfg+                   ]++-- ---------------------------------------------------------------------+ data PluginDescriptor (ideState :: *) =   PluginDescriptor { pluginId           :: !PluginId                    -- ^ Unique identifier of the plugin.@@ -220,21 +322,22 @@ -- which can be inferred from handlers registered by the plugin. -- @config@ is called custom config, which is defined using 'Properties'. data ConfigDescriptor = ConfigDescriptor {-  -- | Whether or not to generate generic configs.-  configEnableGenericConfig :: Bool,+  -- | Initial values for the generic config+  configInitialGenericConfig :: PluginConfig,   -- | Whether or not to generate @diagnosticsOn@ config.   -- Diagnostics emit in arbitrary shake rules,   -- so we can't know statically if the plugin produces diagnostics-  configHasDiagnostics      :: Bool,+  configHasDiagnostics       :: Bool,   -- | Custom config.-  configCustomConfig        :: CustomConfig+  configCustomConfig         :: CustomConfig }  mkCustomConfig :: Properties r -> CustomConfig mkCustomConfig = CustomConfig  defaultConfigDescriptor :: ConfigDescriptor-defaultConfigDescriptor = ConfigDescriptor True False (mkCustomConfig emptyProperties)+defaultConfigDescriptor =+    ConfigDescriptor Data.Default.def False (mkCustomConfig emptyProperties)  -- | Methods that can be handled by plugins. -- 'ExtraParams' captures any extra data the IDE passes to the handlers for this method@@ -281,7 +384,7 @@    default pluginEnabled :: (HasTextDocument (MessageParams m) doc, HasUri doc Uri)                               => SMethod m -> MessageParams m -> PluginDescriptor c -> Config -> Bool-  pluginEnabled _ params desc conf = pluginResponsible uri desc && plcGlobalOn (configForPlugin conf (pluginId desc))+  pluginEnabled _ params desc conf = pluginResponsible uri desc && plcGlobalOn (configForPlugin conf desc)     where         uri = params ^. J.textDocument . J.uri @@ -313,7 +416,7 @@  instance PluginMethod Request TextDocumentCodeAction where   pluginEnabled _ msgParams pluginDesc config =-    pluginResponsible uri pluginDesc && pluginEnabledConfig plcCodeActionsOn (pluginId pluginDesc) config+    pluginResponsible uri pluginDesc && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc)     where       uri = msgParams ^. J.textDocument . J.uri @@ -374,72 +477,76 @@  instance PluginMethod Request TextDocumentCodeLens where   pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcCodeLensOn (pluginId pluginDesc) config+      && pluginEnabledConfig plcCodeLensOn (configForPlugin config pluginDesc)     where       uri = msgParams ^. J.textDocument . J.uri  instance PluginMethod Request TextDocumentRename where   pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcRenameOn (pluginId pluginDesc) config+      && pluginEnabledConfig plcRenameOn (configForPlugin config pluginDesc)    where       uri = msgParams ^. J.textDocument . J.uri instance PluginMethod Request TextDocumentHover where   pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcHoverOn (pluginId pluginDesc) config+      && pluginEnabledConfig plcHoverOn (configForPlugin config pluginDesc)    where       uri = msgParams ^. J.textDocument . J.uri  instance PluginMethod Request TextDocumentDocumentSymbol where   pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcSymbolsOn (pluginId pluginDesc) config+      && pluginEnabledConfig plcSymbolsOn (configForPlugin config pluginDesc)     where       uri = msgParams ^. J.textDocument . J.uri +instance PluginMethod Request CompletionItemResolve where+  pluginEnabled _ msgParams pluginDesc config = pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc)+ instance PluginMethod Request TextDocumentCompletion where   pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcCompletionOn (pluginId pluginDesc) config+      && pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc)     where       uri = msgParams ^. J.textDocument . J.uri  instance PluginMethod Request TextDocumentFormatting where   pluginEnabled STextDocumentFormatting msgParams pluginDesc conf =-    pluginResponsible uri pluginDesc && PluginId (formattingProvider conf) == pid+    pluginResponsible uri pluginDesc+      && (PluginId (formattingProvider conf) == pid || PluginId (cabalFormattingProvider conf) == pid)     where       uri = msgParams ^. J.textDocument . J.uri       pid = pluginId pluginDesc  instance PluginMethod Request TextDocumentRangeFormatting where   pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc-      && PluginId (formattingProvider conf) == pid+      && (PluginId (formattingProvider conf) == pid || PluginId (cabalFormattingProvider conf) == pid)     where       uri = msgParams ^. J.textDocument . J.uri       pid = pluginId pluginDesc  instance PluginMethod Request TextDocumentPrepareCallHierarchy where   pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcCallHierarchyOn pid conf+      && pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc)     where       uri = msgParams ^. J.textDocument . J.uri-      pid = pluginId pluginDesc  instance PluginMethod Request TextDocumentSelectionRange where   pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc-      && pluginEnabledConfig plcSelectionRangeOn pid conf+      && pluginEnabledConfig plcSelectionRangeOn (configForPlugin conf pluginDesc)     where       uri = msgParams ^. J.textDocument . J.uri-      pid = pluginId pluginDesc +instance PluginMethod Request TextDocumentFoldingRange where+  pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc+      && pluginEnabledConfig plcFoldingRangeOn (configForPlugin conf pluginDesc)+    where+      uri = msgParams ^. J.textDocument . J.uri+ instance PluginMethod Request CallHierarchyIncomingCalls where   -- This method has no URI parameter, thus no call to 'pluginResponsible'-  pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn pid conf-    where-      pid = pluginId pluginDesc+  pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc)  instance PluginMethod Request CallHierarchyOutgoingCalls where   -- This method has no URI parameter, thus no call to 'pluginResponsible'-  pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn pid conf-    where-      pid = pluginId pluginDesc+  pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc)  instance PluginMethod Request CustomMethod where   pluginEnabled _ _ _ _ = True@@ -490,6 +597,18 @@             si = SymbolInformation name' (ds ^. kind) Nothing (ds ^. deprecated) loc parent         in [si] <> children' +instance PluginRequestMethod CompletionItemResolve where+  -- resolving completions can only change the detail, additionalTextEdit or documentation fields+  combineResponses _ _ _ _ (x :| xs) = go x xs+    where go :: CompletionItem -> [CompletionItem] -> CompletionItem+          go !comp [] = comp+          go !comp1 (comp2:xs)+            = go (comp1+                 & J.detail              .~ comp1 ^. J.detail <> comp2 ^. J.detail+                 & J.documentation       .~ ((comp1 ^. J.documentation) <|> (comp2 ^. J.documentation)) -- difficult to write generic concatentation for docs+                 & J.additionalTextEdits .~ comp1 ^. J.additionalTextEdits <> comp2 ^. J.additionalTextEdits)+                 xs+ instance PluginRequestMethod TextDocumentCompletion where   combineResponses _ conf _ _ (toList -> xs) = snd $ consumeCompletionResponse limit $ combine xs       where@@ -529,6 +648,9 @@ instance PluginRequestMethod TextDocumentSelectionRange where   combineResponses _ _ _ _ (x :| _) = x +instance PluginRequestMethod TextDocumentFoldingRange where+  combineResponses _ _ _ _ x = sconcat x+ instance PluginRequestMethod CallHierarchyIncomingCalls where  instance PluginRequestMethod CallHierarchyOutgoingCalls where@@ -555,19 +677,19 @@  instance PluginMethod Notification WorkspaceDidChangeWatchedFiles where   -- This method has no URI parameter, thus no call to 'pluginResponsible'.-  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf (pluginId desc)+  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc  instance PluginMethod Notification WorkspaceDidChangeWorkspaceFolders where   -- This method has no URI parameter, thus no call to 'pluginResponsible'.-  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf (pluginId desc)+  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc  instance PluginMethod Notification WorkspaceDidChangeConfiguration where   -- This method has no URI parameter, thus no call to 'pluginResponsible'.-  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf (pluginId desc)+  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc  instance PluginMethod Notification Initialized where   -- This method has no URI parameter, thus no call to 'pluginResponsible'.-  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf (pluginId desc)+  pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc   instance PluginNotificationMethod TextDocumentDidOpen where@@ -727,16 +849,15 @@ instance IsString PluginId where   fromString = PluginId . T.pack -configForPlugin :: Config -> PluginId -> PluginConfig-configForPlugin config (PluginId plugin)-    = Map.findWithDefault Data.Default.def plugin (plugins config)+-- | Lookup the current config for a plugin+configForPlugin :: Config -> PluginDescriptor c -> PluginConfig+configForPlugin config PluginDescriptor{..}+    = Map.findWithDefault (configInitialGenericConfig pluginConfigDescriptor) pluginId (plugins config)  -- | Checks that a given plugin is both enabled and the specific feature is -- enabled-pluginEnabledConfig :: (PluginConfig -> Bool) -> PluginId -> Config -> Bool-pluginEnabledConfig f pid config = plcGlobalOn pluginConfig && f pluginConfig-  where-    pluginConfig = configForPlugin config pid+pluginEnabledConfig :: (PluginConfig -> Bool) -> PluginConfig -> Bool+pluginEnabledConfig f pluginConfig = plcGlobalOn pluginConfig && f pluginConfig  -- --------------------------------------------------------------------- @@ -823,6 +944,7 @@   traceWithSpan sp (WorkspaceSymbolParams _ _ query) = setTag sp "query" (encodeUtf8 query) instance HasTracing CallHierarchyIncomingCallsParams instance HasTracing CallHierarchyOutgoingCallsParams+instance HasTracing CompletionItem  -- --------------------------------------------------------------------- 
test/Ide/PluginUtilsTest.hs view
@@ -1,13 +1,86 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications  #-}+ module Ide.PluginUtilsTest     ( tests     ) where -import           Ide.PluginUtils    (positionInRange)-import           Language.LSP.Types (Position (Position), Range (Range))+import           Data.Char             (isPrint)+import qualified Data.Set              as Set+import qualified Data.Text             as T+import qualified Ide.Plugin.RangeMap   as RangeMap+import           Ide.PluginUtils       (positionInRange, unescape)+import           Language.LSP.Types    (Position (..), Range (Range), UInt,+                                        isSubrangeOf) import           Test.Tasty import           Test.Tasty.HUnit+import           Test.Tasty.QuickCheck  tests :: TestTree tests = testGroup "PluginUtils"-    [+    [ unescapeTest+    , localOption (QuickCheckMaxSize 10000) $+        testProperty "RangeMap-List filtering identical" $+          prop_rangemapListEq @Int     ]++unescapeTest :: TestTree+unescapeTest = testGroup "unescape"+    [ testCase "no double quote" $+        unescape "hello世界" @?= "hello世界"+    , testCase "whole string quoted" $+        unescape "\"hello\\19990\\30028\"" @?= "\"hello世界\""+    , testCase "text before quotes should not be unescaped" $+        unescape "\\19990a\"hello\\30028\"" @?= "\\19990a\"hello界\""+    , testCase "some text after quotes" $+        unescape "\"hello\\19990\\30028\"abc" @?= "\"hello世界\"abc"+    , testCase "many pairs of quote" $+        unescape "oo\"hello\\19990\\30028\"abc\"\1087\1088\1080\1074\1077\1090\"hh" @?= "oo\"hello世界\"abc\"привет\"hh"+    , testCase "double quote itself should not be unescaped" $+        unescape "\"\\\"\\19990o\"" @?= "\"\\\"世o\""+    , testCase "control characters should not be escaped" $+        unescape "\"\\n\\t\"" @?= "\"\\n\\t\""+    ]++genRange :: Gen Range+genRange = oneof [ genRangeInline, genRangeMultiline ]++genRangeInline :: Gen Range+genRangeInline = do+  x1 <- genPosition+  delta <- genRangeLength+  let x2 = x1 { _character = _character x1 + delta }+  pure $ Range x1 x2+  where+    genRangeLength :: Gen UInt+    genRangeLength = fromInteger <$> chooseInteger (5, 50)++genRangeMultiline :: Gen Range+genRangeMultiline = do+  x1 <- genPosition+  let heightDelta = 1+  secondX <- genSecond+  let x2 = x1 { _line = _line x1 + heightDelta+              , _character = secondX+              }+  pure $ Range x1 x2+  where+    genSecond :: Gen UInt+    genSecond = fromInteger <$> chooseInteger (0, 10)++genPosition :: Gen Position+genPosition = Position+  <$> (fromInteger <$> chooseInteger (0, 1000))+  <*> (fromInteger <$> chooseInteger (0, 150))++instance Arbitrary Range where+  arbitrary = genRange++prop_rangemapListEq :: (Show a, Eq a, Ord a) => Range -> [(Range, a)] -> Property+prop_rangemapListEq r xs =+  let filteredList = (map snd . filter (isSubrangeOf r . fst)) xs+      filteredRangeMap = RangeMap.filterByRange r (RangeMap.fromList' xs)+   in classify (null filteredList) "no matches" $+      cover 5 (length filteredList == 1) "1 match" $+      cover 2 (length filteredList > 1) ">1 matches" $+      Set.fromList filteredList === Set.fromList filteredRangeMap