ghcide 2.0.0.1 → 2.1.0.0
raw patch · 88 files changed
+5761/−4833 lines, 88 filesdep +row-typesdep ~hls-graphdep ~hls-plugin-apidep ~lspPVP ok
version bump matches the API change (PVP)
Dependencies added: row-types
Dependency ranges changed: hls-graph, hls-plugin-api, lsp, lsp-test, lsp-types
API changes (from Hackage documentation)
- Development.IDE: DsError :: DiagnosticSeverity
- Development.IDE: DsHint :: DiagnosticSeverity
- Development.IDE: DsInfo :: DiagnosticSeverity
- Development.IDE: DsWarning :: DiagnosticSeverity
- Development.IDE: GetDependencyInformation :: GetDependencyInformation
- Development.IDE: List :: [a] -> List a
- Development.IDE: [_character] :: Position -> UInt
- Development.IDE: [_end] :: Range -> Position
- Development.IDE: [_line] :: Position -> UInt
- Development.IDE: [_range] :: Location -> Range
- Development.IDE: [_start] :: Range -> Position
- Development.IDE: [_uri] :: Location -> Uri
- Development.IDE: [getUri] :: Uri -> Text
- Development.IDE: data GetDependencyInformation
- Development.IDE: newtype List a
- Development.IDE: useE :: IdeRule k v => k -> NormalizedFilePath -> MaybeT IdeAction (v, PositionMapping)
- Development.IDE: useNoFileE :: IdeRule k v => IdeState -> k -> MaybeT IdeAction v
- Development.IDE: usesE :: IdeRule k v => k -> [NormalizedFilePath] -> MaybeT IdeAction [(v, PositionMapping)]
- Development.IDE: withDefaultRecorder :: MonadUnliftIO m => Maybe FilePath -> Maybe [LoggingColumn] -> (Recorder (WithPriority (Doc d)) -> m a) -> m a
- Development.IDE.Core.Actions: useE :: IdeRule k v => k -> NormalizedFilePath -> MaybeT IdeAction (v, PositionMapping)
- Development.IDE.Core.Actions: useNoFileE :: IdeRule k v => IdeState -> k -> MaybeT IdeAction v
- Development.IDE.Core.Actions: usesE :: IdeRule k v => k -> [NormalizedFilePath] -> MaybeT IdeAction [(v, PositionMapping)]
- Development.IDE.Core.RuleTypes: GetDependencyInformation :: GetDependencyInformation
- Development.IDE.Core.RuleTypes: data GetDependencyInformation
- Development.IDE.Core.RuleTypes: instance Control.DeepSeq.NFData Development.IDE.Core.RuleTypes.GetDependencyInformation
- Development.IDE.Core.RuleTypes: instance Data.Hashable.Class.Hashable Development.IDE.Core.RuleTypes.GetDependencyInformation
- Development.IDE.Core.RuleTypes: instance GHC.Classes.Eq Development.IDE.Core.RuleTypes.GetDependencyInformation
- Development.IDE.Core.RuleTypes: instance GHC.Generics.Generic Development.IDE.Core.RuleTypes.GetDependencyInformation
- Development.IDE.Core.RuleTypes: instance GHC.Show.Show Development.IDE.Core.RuleTypes.GetDependencyInformation
- Development.IDE.Core.Rules: [$sel:checkForImportCycles:GhcSessionDepsConfig] :: GhcSessionDepsConfig -> Bool
- Development.IDE.Core.Rules: [$sel:checkForImportCycles:RulesConfig] :: RulesConfig -> Bool
- Development.IDE.Core.Rules: getDependencies :: NormalizedFilePath -> Action (Maybe [NormalizedFilePath])
- Development.IDE.Core.Rules: getDependencyInformationRule :: Recorder (WithPriority Log) -> Rules ()
- Development.IDE.Core.UseStale: instance Development.IDE.Core.UseStale.MapAge Language.LSP.Types.Location.Range
- Development.IDE.Import.DependencyInformation: ShowableModuleName :: ModuleName -> ShowableModuleName
- Development.IDE.Import.DependencyInformation: [depModuleNames] :: DependencyInformation -> !FilePathIdMap ShowableModuleName
- Development.IDE.Import.DependencyInformation: [rawBootMap] :: RawDependencyInformation -> !BootIdMap
- Development.IDE.Import.DependencyInformation: [rawModuleNameMap] :: RawDependencyInformation -> !FilePathIdMap ShowableModuleName
- Development.IDE.Import.DependencyInformation: [showableModuleName] :: ShowableModuleName -> ModuleName
- Development.IDE.Import.DependencyInformation: instance Control.DeepSeq.NFData Development.IDE.Import.DependencyInformation.ShowableModuleName
- Development.IDE.Import.DependencyInformation: instance GHC.Show.Show Development.IDE.Import.DependencyInformation.ShowableModuleName
- Development.IDE.Import.DependencyInformation: newtype ShowableModuleName
- Development.IDE.LSP.Server: [$sel:unServerM:ServerM] :: ServerM c a -> ReaderT (ReactorChan, IdeState) (LspM c) a
- Development.IDE.Types.Diagnostics: DsError :: DiagnosticSeverity
- Development.IDE.Types.Diagnostics: DsHint :: DiagnosticSeverity
- Development.IDE.Types.Diagnostics: DsInfo :: DiagnosticSeverity
- Development.IDE.Types.Diagnostics: DsWarning :: DiagnosticSeverity
- Development.IDE.Types.Diagnostics: List :: [a] -> List a
- Development.IDE.Types.Diagnostics: newtype List a
- Development.IDE.Types.Location: [_character] :: Position -> UInt
- Development.IDE.Types.Location: [_end] :: Range -> Position
- Development.IDE.Types.Location: [_line] :: Position -> UInt
- Development.IDE.Types.Location: [_range] :: Location -> Range
- Development.IDE.Types.Location: [_start] :: Range -> Position
- Development.IDE.Types.Location: [_uri] :: Location -> Uri
- Development.IDE.Types.Location: [getUri] :: Uri -> Text
- Development.IDE.Types.Logger: DataColumn :: LoggingColumn
- Development.IDE.Types.Logger: Debug :: Priority
- Development.IDE.Types.Logger: Error :: Priority
- Development.IDE.Types.Logger: Info :: Priority
- Development.IDE.Types.Logger: Logger :: (Priority -> Text -> IO ()) -> Logger
- Development.IDE.Types.Logger: PriorityColumn :: LoggingColumn
- Development.IDE.Types.Logger: Recorder :: (forall m. MonadIO m => msg -> m ()) -> Recorder msg
- Development.IDE.Types.Logger: SourceLocColumn :: LoggingColumn
- Development.IDE.Types.Logger: ThreadIdColumn :: LoggingColumn
- Development.IDE.Types.Logger: TimeColumn :: LoggingColumn
- Development.IDE.Types.Logger: Warning :: Priority
- Development.IDE.Types.Logger: WithPriority :: Priority -> CallStack -> a -> WithPriority a
- Development.IDE.Types.Logger: [callStack_] :: WithPriority a -> CallStack
- Development.IDE.Types.Logger: [logPriority] :: Logger -> Priority -> Text -> IO ()
- Development.IDE.Types.Logger: [logger_] :: Recorder msg -> forall m. MonadIO m => msg -> m ()
- Development.IDE.Types.Logger: [payload] :: WithPriority a -> a
- Development.IDE.Types.Logger: [priority] :: WithPriority a -> Priority
- Development.IDE.Types.Logger: cfilter :: (a -> Bool) -> Recorder a -> Recorder a
- Development.IDE.Types.Logger: cmap :: (a -> b) -> Recorder b -> Recorder a
- Development.IDE.Types.Logger: cmapIO :: (a -> IO b) -> Recorder b -> Recorder a
- Development.IDE.Types.Logger: cmapWithPrio :: (a -> b) -> Recorder (WithPriority b) -> Recorder (WithPriority a)
- Development.IDE.Types.Logger: data LoggingColumn
- Development.IDE.Types.Logger: data Priority
- Development.IDE.Types.Logger: data WithPriority a
- Development.IDE.Types.Logger: instance Data.Functor.Contravariant.Contravariant Development.IDE.Types.Logger.Recorder
- Development.IDE.Types.Logger: instance GHC.Base.Functor Development.IDE.Types.Logger.WithPriority
- Development.IDE.Types.Logger: instance GHC.Base.Monoid (Development.IDE.Types.Logger.Recorder msg)
- Development.IDE.Types.Logger: instance GHC.Base.Monoid Development.IDE.Types.Logger.Logger
- Development.IDE.Types.Logger: instance GHC.Base.Semigroup (Development.IDE.Types.Logger.Recorder msg)
- Development.IDE.Types.Logger: instance GHC.Base.Semigroup Development.IDE.Types.Logger.Logger
- Development.IDE.Types.Logger: instance GHC.Classes.Eq Development.IDE.Types.Logger.Priority
- Development.IDE.Types.Logger: instance GHC.Classes.Ord Development.IDE.Types.Logger.Priority
- Development.IDE.Types.Logger: instance GHC.Enum.Bounded Development.IDE.Types.Logger.Priority
- Development.IDE.Types.Logger: instance GHC.Enum.Enum Development.IDE.Types.Logger.Priority
- Development.IDE.Types.Logger: instance GHC.Show.Show Development.IDE.Types.Logger.Priority
- Development.IDE.Types.Logger: logDebug :: Logger -> Text -> IO ()
- Development.IDE.Types.Logger: logError :: Logger -> Text -> IO ()
- Development.IDE.Types.Logger: logInfo :: Logger -> Text -> IO ()
- Development.IDE.Types.Logger: logWarning :: Logger -> Text -> IO ()
- Development.IDE.Types.Logger: logWith :: (HasCallStack, MonadIO m) => Recorder (WithPriority msg) -> Priority -> msg -> m ()
- Development.IDE.Types.Logger: lspClientLogRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)
- Development.IDE.Types.Logger: lspClientMessageRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)
- Development.IDE.Types.Logger: makeDefaultHandleRecorder :: MonadIO m => Maybe [LoggingColumn] -> Lock -> Handle -> m (Recorder (WithPriority (Doc a)))
- Development.IDE.Types.Logger: makeDefaultStderrRecorder :: MonadIO m => Maybe [LoggingColumn] -> m (Recorder (WithPriority (Doc a)))
- Development.IDE.Types.Logger: newtype Logger
- Development.IDE.Types.Logger: newtype Recorder msg
- Development.IDE.Types.Logger: noLogging :: Logger
- Development.IDE.Types.Logger: renderStrict :: SimpleDocStream ann -> Text
- Development.IDE.Types.Logger: toCologActionWithPrio :: (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)
- Development.IDE.Types.Logger: withBacklog :: (v -> Recorder a) -> IO (Recorder a, v -> IO ())
- Development.IDE.Types.Logger: withDefaultRecorder :: MonadUnliftIO m => Maybe FilePath -> Maybe [LoggingColumn] -> (Recorder (WithPriority (Doc d)) -> m a) -> m a
+ Development.IDE: DiagnosticSeverity_Error :: DiagnosticSeverity
+ Development.IDE: DiagnosticSeverity_Hint :: DiagnosticSeverity
+ Development.IDE: DiagnosticSeverity_Information :: DiagnosticSeverity
+ Development.IDE: DiagnosticSeverity_Warning :: DiagnosticSeverity
+ Development.IDE: [$sel:_character:Position] :: Position -> UInt
+ Development.IDE: [$sel:_codeDescription:Diagnostic] :: Diagnostic -> Maybe CodeDescription
+ Development.IDE: [$sel:_data_:Diagnostic] :: Diagnostic -> Maybe Value
+ Development.IDE: [$sel:_end:Range] :: Range -> Position
+ Development.IDE: [$sel:_line:Position] :: Position -> UInt
+ Development.IDE: [$sel:_range:Location] :: Location -> Range
+ Development.IDE: [$sel:_start:Range] :: Range -> Position
+ Development.IDE: [$sel:_uri:Location] :: Location -> Uri
+ Development.IDE: [$sel:getUri:Uri] :: Uri -> Text
+ Development.IDE: withFileRecorder :: MonadUnliftIO m => FilePath -> Maybe [LoggingColumn] -> (Either IOException (Recorder (WithPriority (Doc d))) -> m a) -> m a
+ Development.IDE.Core.Compile: shareUsages :: ModIface -> ModIface
+ Development.IDE.Core.Compile: sourceParser :: Text
+ Development.IDE.Core.Compile: sourceTypecheck :: Text
+ Development.IDE.Core.PluginUtils: fromCurrentPositionE :: Monad m => PositionMapping -> Position -> ExceptT PluginError m Position
+ Development.IDE.Core.PluginUtils: fromCurrentPositionMT :: Monad m => PositionMapping -> Position -> MaybeT m Position
+ Development.IDE.Core.PluginUtils: fromCurrentRangeE :: Monad m => PositionMapping -> Range -> ExceptT PluginError m Range
+ Development.IDE.Core.PluginUtils: fromCurrentRangeMT :: Monad m => PositionMapping -> Range -> MaybeT m Range
+ Development.IDE.Core.PluginUtils: runActionE :: MonadIO m => String -> IdeState -> ExceptT e Action a -> ExceptT e m a
+ Development.IDE.Core.PluginUtils: runActionMT :: MonadIO m => String -> IdeState -> MaybeT Action a -> MaybeT m a
+ Development.IDE.Core.PluginUtils: runIdeActionE :: MonadIO m => String -> ShakeExtras -> ExceptT e IdeAction a -> ExceptT e m a
+ Development.IDE.Core.PluginUtils: runIdeActionMT :: MonadIO m => String -> ShakeExtras -> MaybeT IdeAction a -> MaybeT m a
+ Development.IDE.Core.PluginUtils: toCurrentPositionE :: Monad m => PositionMapping -> Position -> ExceptT PluginError m Position
+ Development.IDE.Core.PluginUtils: toCurrentPositionMT :: Monad m => PositionMapping -> Position -> MaybeT m Position
+ Development.IDE.Core.PluginUtils: toCurrentRangeE :: Monad m => PositionMapping -> Range -> ExceptT PluginError m Range
+ Development.IDE.Core.PluginUtils: toCurrentRangeMT :: Monad m => PositionMapping -> Range -> MaybeT m Range
+ Development.IDE.Core.PluginUtils: uriToFilePathE :: Monad m => Uri -> ExceptT PluginError m FilePath
+ Development.IDE.Core.PluginUtils: uriToFilePathMT :: Monad m => Uri -> MaybeT m FilePath
+ Development.IDE.Core.PluginUtils: useE :: IdeRule k v => k -> NormalizedFilePath -> ExceptT PluginError Action v
+ Development.IDE.Core.PluginUtils: useMT :: IdeRule k v => k -> NormalizedFilePath -> MaybeT Action v
+ Development.IDE.Core.PluginUtils: useWithStaleE :: IdeRule k v => k -> NormalizedFilePath -> ExceptT PluginError Action (v, PositionMapping)
+ Development.IDE.Core.PluginUtils: useWithStaleFastE :: IdeRule k v => k -> NormalizedFilePath -> ExceptT PluginError IdeAction (v, PositionMapping)
+ Development.IDE.Core.PluginUtils: useWithStaleFastMT :: IdeRule k v => k -> NormalizedFilePath -> MaybeT IdeAction (v, PositionMapping)
+ Development.IDE.Core.PluginUtils: useWithStaleMT :: IdeRule k v => k -> NormalizedFilePath -> MaybeT Action (v, PositionMapping)
+ Development.IDE.Core.PluginUtils: usesE :: (Traversable f, IdeRule k v) => k -> f NormalizedFilePath -> ExceptT PluginError Action (f v)
+ Development.IDE.Core.PluginUtils: usesMT :: (Traversable f, IdeRule k v) => k -> f NormalizedFilePath -> MaybeT Action (f v)
+ Development.IDE.Core.Rules: [$sel:fullModuleGraph:GhcSessionDepsConfig] :: GhcSessionDepsConfig -> Bool
+ Development.IDE.Core.Rules: [$sel:fullModuleGraph:RulesConfig] :: RulesConfig -> Bool
+ Development.IDE.Core.UseStale: instance Development.IDE.Core.UseStale.MapAge Language.LSP.Protocol.Internal.Types.Range.Range
+ Development.IDE.GHC.Compat.Core: readIface :: Module -> FilePath -> TcRnIf gbl lcl (MaybeErr SDoc ModIface)
+ Development.IDE.GHC.Compat.Units: findImportedModule :: HscEnv -> ModuleName -> IO (Maybe Module)
+ Development.IDE.GHC.Orphans: instance Control.DeepSeq.NFData GHC.Unit.Module.Graph.ModuleGraph
+ Development.IDE.GHC.Orphans: instance GHC.Show.Show GHC.Unit.Module.Graph.ModuleGraph
+ Development.IDE.Import.DependencyInformation: PathIdMap :: !FilePathIdMap ArtifactsLocation -> !HashMap NormalizedFilePath FilePathId -> !Int -> PathIdMap
+ Development.IDE.Import.DependencyInformation: ShowableModule :: Module -> ShowableModule
+ Development.IDE.Import.DependencyInformation: ShowableModuleEnv :: ModuleEnv a -> ShowableModuleEnv a
+ Development.IDE.Import.DependencyInformation: [depModuleFiles] :: DependencyInformation -> !ShowableModuleEnv FilePathId
+ Development.IDE.Import.DependencyInformation: [depModuleGraph] :: DependencyInformation -> !ModuleGraph
+ Development.IDE.Import.DependencyInformation: [depModules] :: DependencyInformation -> !FilePathIdMap ShowableModule
+ Development.IDE.Import.DependencyInformation: [idToPathMap] :: PathIdMap -> !FilePathIdMap ArtifactsLocation
+ Development.IDE.Import.DependencyInformation: [nextFreshId] :: PathIdMap -> !Int
+ Development.IDE.Import.DependencyInformation: [pathToIdMap] :: PathIdMap -> !HashMap NormalizedFilePath FilePathId
+ Development.IDE.Import.DependencyInformation: [rawModuleMap] :: RawDependencyInformation -> !FilePathIdMap ShowableModule
+ Development.IDE.Import.DependencyInformation: [showableModuleEnv] :: ShowableModuleEnv a -> ModuleEnv a
+ Development.IDE.Import.DependencyInformation: [showableModule] :: ShowableModule -> Module
+ Development.IDE.Import.DependencyInformation: instance Control.DeepSeq.NFData Development.IDE.Import.DependencyInformation.ShowableModule
+ Development.IDE.Import.DependencyInformation: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Development.IDE.Import.DependencyInformation.ShowableModuleEnv a)
+ Development.IDE.Import.DependencyInformation: instance GHC.Show.Show Development.IDE.Import.DependencyInformation.ShowableModule
+ Development.IDE.Import.DependencyInformation: instance GHC.Show.Show a => GHC.Show.Show (Development.IDE.Import.DependencyInformation.ShowableModuleEnv a)
+ Development.IDE.Import.DependencyInformation: lookupModuleFile :: Module -> DependencyInformation -> Maybe NormalizedFilePath
+ Development.IDE.Import.DependencyInformation: newtype ShowableModule
+ Development.IDE.Import.DependencyInformation: newtype ShowableModuleEnv a
+ Development.IDE.LSP.Server: [unServerM] :: ServerM c a -> ReaderT (ReactorChan, IdeState) (LspM c) a
+ Development.IDE.Plugin.HLS: ExceptionInPlugin :: PluginId -> Some SMethod -> SomeException -> Log
+ Development.IDE.Plugin.HLS: LogResponseError :: PluginId -> ResponseError -> Log
+ Development.IDE.Plugin.HLS: toResponseError :: (PluginId, PluginError) -> ResponseError
+ Development.IDE.Types.Diagnostics: DiagnosticSeverity_Error :: DiagnosticSeverity
+ Development.IDE.Types.Diagnostics: DiagnosticSeverity_Hint :: DiagnosticSeverity
+ Development.IDE.Types.Diagnostics: DiagnosticSeverity_Information :: DiagnosticSeverity
+ Development.IDE.Types.Diagnostics: DiagnosticSeverity_Warning :: DiagnosticSeverity
+ Development.IDE.Types.Diagnostics: [$sel:_codeDescription:Diagnostic] :: Diagnostic -> Maybe CodeDescription
+ Development.IDE.Types.Diagnostics: [$sel:_data_:Diagnostic] :: Diagnostic -> Maybe Value
+ Development.IDE.Types.HscEnvEq: updateHscEnvEq :: HscEnvEq -> HscEnv -> IO HscEnvEq
+ Development.IDE.Types.Location: [$sel:_character:Position] :: Position -> UInt
+ Development.IDE.Types.Location: [$sel:_end:Range] :: Range -> Position
+ Development.IDE.Types.Location: [$sel:_line:Position] :: Position -> UInt
+ Development.IDE.Types.Location: [$sel:_range:Location] :: Location -> Range
+ Development.IDE.Types.Location: [$sel:_start:Range] :: Range -> Position
+ Development.IDE.Types.Location: [$sel:_uri:Location] :: Location -> Uri
+ Development.IDE.Types.Location: [$sel:getUri:Uri] :: Uri -> Text
+ Text.Fuzzy.Parallel: defChunkSize :: Int
+ Text.Fuzzy.Parallel: defMaxResults :: Int
+ Text.Fuzzy.Parallel: filter' :: Int -> Int -> Text -> [t] -> (t -> Text) -> (Text -> Text -> Maybe Int) -> [Scored t]
+ Text.Fuzzy.Parallel: simpleFilter' :: Int -> Int -> Text -> [Text] -> (Text -> Text -> Maybe Int) -> [Scored Text]
- Development.IDE: Diagnostic :: Range -> Maybe DiagnosticSeverity -> Maybe (Int32 |? Text) -> Maybe DiagnosticSource -> Text -> Maybe (List DiagnosticTag) -> Maybe (List DiagnosticRelatedInformation) -> Diagnostic
+ Development.IDE: Diagnostic :: Range -> Maybe DiagnosticSeverity -> Maybe (Int32 |? Text) -> Maybe CodeDescription -> Maybe Text -> Text -> Maybe [DiagnosticTag] -> Maybe [DiagnosticRelatedInformation] -> Maybe Value -> Diagnostic
- Development.IDE: Recorder :: (forall m. MonadIO m => msg -> m ()) -> Recorder msg
+ Development.IDE: Recorder :: (forall (m :: Type -> Type). MonadIO m => msg -> m ()) -> Recorder msg
- Development.IDE: [$sel:_relatedInformation:Diagnostic] :: Diagnostic -> Maybe (List DiagnosticRelatedInformation)
+ Development.IDE: [$sel:_relatedInformation:Diagnostic] :: Diagnostic -> Maybe [DiagnosticRelatedInformation]
- Development.IDE: [$sel:_source:Diagnostic] :: Diagnostic -> Maybe DiagnosticSource
+ Development.IDE: [$sel:_source:Diagnostic] :: Diagnostic -> Maybe Text
- Development.IDE: [$sel:_tags:Diagnostic] :: Diagnostic -> Maybe (List DiagnosticTag)
+ Development.IDE: [$sel:_tags:Diagnostic] :: Diagnostic -> Maybe [DiagnosticTag]
- Development.IDE: [logger_] :: Recorder msg -> forall m. MonadIO m => msg -> m ()
+ Development.IDE: [logger_] :: Recorder msg -> forall (m :: Type -> Type). MonadIO m => msg -> m ()
- Development.IDE: ideErrorWithSource :: Maybe DiagnosticSource -> Maybe DiagnosticSeverity -> a -> Text -> (a, ShowDiagnostic, Diagnostic)
+ Development.IDE: ideErrorWithSource :: Maybe Text -> Maybe DiagnosticSeverity -> a -> Text -> (a, ShowDiagnostic, Diagnostic)
- Development.IDE: toCologActionWithPrio :: (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)
+ Development.IDE: toCologActionWithPrio :: forall (m :: Type -> Type) msg. (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)
- Development.IDE.Core.Compile: mergeEnvs :: HscEnv -> ModSummary -> [HomeModInfo] -> [HscEnv] -> IO HscEnv
+ Development.IDE.Core.Compile: mergeEnvs :: HscEnv -> ModuleGraph -> ModSummary -> [HomeModInfo] -> [HscEnv] -> IO HscEnv
- Development.IDE.Core.Service: updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> List TextDocumentContentChangeEvent -> STM ()
+ Development.IDE.Core.Service: updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> STM ()
- Development.IDE.Core.Shake: addPersistentRule :: IdeRule k v => k -> (NormalizedFilePath -> IdeAction (Maybe (v, PositionDelta, TextDocumentVersion))) -> Rules ()
+ Development.IDE.Core.Shake: addPersistentRule :: IdeRule k v => k -> (NormalizedFilePath -> IdeAction (Maybe (v, PositionDelta, Maybe Int32))) -> Rules ()
- Development.IDE.Core.Shake: updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> List TextDocumentContentChangeEvent -> STM ()
+ Development.IDE.Core.Shake: updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> STM ()
- Development.IDE.Import.DependencyInformation: DependencyInformation :: !FilePathIdMap (NonEmpty NodeError) -> !FilePathIdMap ShowableModuleName -> !FilePathIdMap FilePathIdSet -> !IntMap IntSet -> !PathIdMap -> !BootIdMap -> DependencyInformation
+ Development.IDE.Import.DependencyInformation: DependencyInformation :: !FilePathIdMap (NonEmpty NodeError) -> !FilePathIdMap ShowableModule -> !FilePathIdMap FilePathIdSet -> !IntMap IntSet -> !PathIdMap -> !BootIdMap -> !ShowableModuleEnv FilePathId -> !ModuleGraph -> DependencyInformation
- Development.IDE.Import.DependencyInformation: RawDependencyInformation :: !FilePathIdMap (Either ModuleParseError ModuleImports) -> !PathIdMap -> !BootIdMap -> !FilePathIdMap ShowableModuleName -> RawDependencyInformation
+ Development.IDE.Import.DependencyInformation: RawDependencyInformation :: !FilePathIdMap (Either ModuleParseError ModuleImports) -> !PathIdMap -> !FilePathIdMap ShowableModule -> RawDependencyInformation
- Development.IDE.Import.DependencyInformation: pathToId :: PathIdMap -> NormalizedFilePath -> FilePathId
+ Development.IDE.Import.DependencyInformation: pathToId :: PathIdMap -> NormalizedFilePath -> Maybe FilePathId
- Development.IDE.Import.DependencyInformation: processDependencyInformation :: RawDependencyInformation -> DependencyInformation
+ Development.IDE.Import.DependencyInformation: processDependencyInformation :: RawDependencyInformation -> BootIdMap -> ModuleGraph -> DependencyInformation
- Development.IDE.LSP.HoverDefinition: documentHighlight :: IdeState -> TextDocumentPositionParams -> LspM c (Either ResponseError (List DocumentHighlight))
+ Development.IDE.LSP.HoverDefinition: documentHighlight :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LspM c) ([DocumentHighlight] |? Null)
- Development.IDE.LSP.HoverDefinition: gotoDefinition :: IdeState -> TextDocumentPositionParams -> LspM c (Either ResponseError (ResponseResult TextDocumentDefinition))
+ Development.IDE.LSP.HoverDefinition: gotoDefinition :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LspM c) (MessageResult Method_TextDocumentDefinition)
- Development.IDE.LSP.HoverDefinition: gotoTypeDefinition :: IdeState -> TextDocumentPositionParams -> LspM c (Either ResponseError (ResponseResult TextDocumentTypeDefinition))
+ Development.IDE.LSP.HoverDefinition: gotoTypeDefinition :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LspM c) (MessageResult Method_TextDocumentTypeDefinition)
- Development.IDE.LSP.HoverDefinition: hover :: IdeState -> TextDocumentPositionParams -> LspM c (Either ResponseError (Maybe Hover))
+ Development.IDE.LSP.HoverDefinition: hover :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LspM c) (Hover |? Null)
- Development.IDE.LSP.HoverDefinition: references :: IdeState -> ReferenceParams -> LspM c (Either ResponseError (List Location))
+ Development.IDE.LSP.HoverDefinition: references :: PluginMethodHandler IdeState 'Method_TextDocumentReferences
- Development.IDE.LSP.HoverDefinition: wsSymbols :: IdeState -> WorkspaceSymbolParams -> LspM c (Either ResponseError (List SymbolInformation))
+ Development.IDE.LSP.HoverDefinition: wsSymbols :: PluginMethodHandler IdeState 'Method_WorkspaceSymbol
- Development.IDE.LSP.LanguageServer: runLanguageServer :: forall config a m. Show config => Recorder (WithPriority Log) -> Options -> Handle -> Handle -> config -> (config -> Value -> Either Text config) -> (MVar () -> IO (LanguageContextEnv config -> RequestMessage Initialize -> IO (Either ResponseError (LanguageContextEnv config, a)), Handlers (m config), (LanguageContextEnv config, a) -> m config <~> IO)) -> IO ()
+ Development.IDE.LSP.LanguageServer: runLanguageServer :: forall config a m. Show config => Recorder (WithPriority Log) -> Options -> Handle -> Handle -> config -> (config -> Value -> Either Text config) -> (MVar () -> IO (LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either ResponseError (LanguageContextEnv config, a)), Handlers (m config), (LanguageContextEnv config, a) -> m config <~> IO)) -> IO ()
- Development.IDE.LSP.LanguageServer: setupLSP :: forall config err. Recorder (WithPriority Log) -> (FilePath -> IO FilePath) -> Handlers (ServerM config) -> (LanguageContextEnv config -> Maybe FilePath -> WithHieDb -> IndexQueue -> IO IdeState) -> MVar () -> IO (LanguageContextEnv config -> RequestMessage Initialize -> IO (Either err (LanguageContextEnv config, IdeState)), Handlers (ServerM config), (LanguageContextEnv config, IdeState) -> ServerM config <~> IO)
+ Development.IDE.LSP.LanguageServer: setupLSP :: forall config err. Recorder (WithPriority Log) -> (FilePath -> IO FilePath) -> Handlers (ServerM config) -> (LanguageContextEnv config -> Maybe FilePath -> WithHieDb -> IndexQueue -> IO IdeState) -> MVar () -> IO (LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either err (LanguageContextEnv config, IdeState)), Handlers (ServerM config), (LanguageContextEnv config, IdeState) -> ServerM config <~> IO)
- Development.IDE.LSP.Outline: moduleOutline :: IdeState -> DocumentSymbolParams -> LspM c (Either ResponseError (List DocumentSymbol |? List SymbolInformation))
+ Development.IDE.LSP.Outline: moduleOutline :: PluginMethodHandler IdeState 'Method_TextDocumentDocumentSymbol
- Development.IDE.LSP.Server: notificationHandler :: forall (m :: Method FromClient Notification) c. HasTracing (MessageParams m) => SMethod m -> (IdeState -> VFS -> MessageParams m -> LspM c ()) -> Handlers (ServerM c)
+ Development.IDE.LSP.Server: notificationHandler :: forall (m :: Method ClientToServer Notification) c. HasTracing (MessageParams m) => SMethod m -> (IdeState -> VFS -> MessageParams m -> LspM c ()) -> Handlers (ServerM c)
- Development.IDE.LSP.Server: requestHandler :: forall (m :: Method FromClient Request) c. HasTracing (MessageParams m) => SMethod m -> (IdeState -> MessageParams m -> LspM c (Either ResponseError (ResponseResult m))) -> Handlers (ServerM c)
+ Development.IDE.LSP.Server: requestHandler :: forall (m :: Method ClientToServer Request) c. HasTracing (MessageParams m) => SMethod m -> (IdeState -> MessageParams m -> LspM c (Either ResponseError (MessageResult m))) -> Handlers (ServerM c)
- Development.IDE.Plugin.HLS: LogPluginError :: PluginId -> ResponseError -> Log
+ Development.IDE.Plugin.HLS: LogPluginError :: PluginId -> PluginError -> Log
- Development.IDE.Spans.AtPoint: atPoint :: IdeOptions -> HieAstResult -> DocAndKindMap -> HscEnv -> Position -> Maybe (Maybe Range, [Text])
+ Development.IDE.Spans.AtPoint: atPoint :: IdeOptions -> HieAstResult -> DocAndKindMap -> HscEnv -> Position -> IO (Maybe (Maybe Range, [Text]))
- Development.IDE.Spans.Pragmas: getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT String m NextPragmaInfo
+ Development.IDE.Spans.Pragmas: getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError m NextPragmaInfo
- Development.IDE.Types.Diagnostics: Diagnostic :: Range -> Maybe DiagnosticSeverity -> Maybe (Int32 |? Text) -> Maybe DiagnosticSource -> Text -> Maybe (List DiagnosticTag) -> Maybe (List DiagnosticRelatedInformation) -> Diagnostic
+ Development.IDE.Types.Diagnostics: Diagnostic :: Range -> Maybe DiagnosticSeverity -> Maybe (Int32 |? Text) -> Maybe CodeDescription -> Maybe Text -> Text -> Maybe [DiagnosticTag] -> Maybe [DiagnosticRelatedInformation] -> Maybe Value -> Diagnostic
- Development.IDE.Types.Diagnostics: [$sel:_relatedInformation:Diagnostic] :: Diagnostic -> Maybe (List DiagnosticRelatedInformation)
+ Development.IDE.Types.Diagnostics: [$sel:_relatedInformation:Diagnostic] :: Diagnostic -> Maybe [DiagnosticRelatedInformation]
- Development.IDE.Types.Diagnostics: [$sel:_source:Diagnostic] :: Diagnostic -> Maybe DiagnosticSource
+ Development.IDE.Types.Diagnostics: [$sel:_source:Diagnostic] :: Diagnostic -> Maybe Text
- Development.IDE.Types.Diagnostics: [$sel:_tags:Diagnostic] :: Diagnostic -> Maybe (List DiagnosticTag)
+ Development.IDE.Types.Diagnostics: [$sel:_tags:Diagnostic] :: Diagnostic -> Maybe [DiagnosticTag]
- Development.IDE.Types.Diagnostics: ideErrorWithSource :: Maybe DiagnosticSource -> Maybe DiagnosticSeverity -> a -> Text -> (a, ShowDiagnostic, Diagnostic)
+ Development.IDE.Types.Diagnostics: ideErrorWithSource :: Maybe Text -> Maybe DiagnosticSeverity -> a -> Text -> (a, ShowDiagnostic, Diagnostic)
Files
- exe/Main.hs +6/−6
- ghcide.cabal +43/−7
- session-loader/Development/IDE/Session.hs +32/−12
- src/Development/IDE.hs +2/−4
- src/Development/IDE/Core/Actions.hs +48/−30
- src/Development/IDE/Core/Compile.hs +116/−83
- src/Development/IDE/Core/FileExists.hs +8/−8
- src/Development/IDE/Core/FileStore.hs +21/−20
- src/Development/IDE/Core/IdeConfiguration.hs +5/−4
- src/Development/IDE/Core/OfInterest.hs +10/−7
- src/Development/IDE/Core/PluginUtils.hs +140/−0
- src/Development/IDE/Core/PositionMapping.hs +13/−8
- src/Development/IDE/Core/Preprocessor.hs +4/−2
- src/Development/IDE/Core/ProgressReporting.hs +22/−16
- src/Development/IDE/Core/RuleTypes.hs +3/−13
- src/Development/IDE/Core/Rules.hs +114/−63
- src/Development/IDE/Core/Service.hs +3/−3
- src/Development/IDE/Core/Shake.hs +44/−26
- src/Development/IDE/Core/Tracing.hs +3/−2
- src/Development/IDE/GHC/Compat/Core.hs +1/−0
- src/Development/IDE/GHC/Compat/Units.hs +47/−21
- src/Development/IDE/GHC/Error.hs +8/−6
- src/Development/IDE/GHC/Orphans.hs +4/−0
- src/Development/IDE/GHC/Warnings.hs +1/−1
- src/Development/IDE/Import/DependencyInformation.hs +34/−15
- src/Development/IDE/Import/FindImports.hs +30/−9
- src/Development/IDE/LSP/HoverDefinition.hs +28/−26
- src/Development/IDE/LSP/LanguageServer.hs +23/−20
- src/Development/IDE/LSP/Notifications.hs +14/−13
- src/Development/IDE/LSP/Outline.hs +38/−37
- src/Development/IDE/LSP/Server.hs +14/−18
- src/Development/IDE/Main.hs +3/−3
- src/Development/IDE/Main/HeapStats.hs +4/−4
- src/Development/IDE/Monitoring/EKG.hs +2/−2
- src/Development/IDE/Plugin/Completions.hs +52/−58
- src/Development/IDE/Plugin/Completions/Logic.hs +55/−46
- src/Development/IDE/Plugin/Completions/Types.hs +8/−8
- src/Development/IDE/Plugin/HLS.hs +145/−85
- src/Development/IDE/Plugin/HLS/GhcIde.hs +10/−16
- src/Development/IDE/Plugin/Test.hs +21/−18
- src/Development/IDE/Plugin/TypeLenses.hs +36/−39
- src/Development/IDE/Spans/AtPoint.hs +65/−20
- src/Development/IDE/Spans/Documentation.hs +1/−1
- src/Development/IDE/Spans/Pragmas.hs +8/−9
- src/Development/IDE/Types/Action.hs +6/−6
- src/Development/IDE/Types/Diagnostics.hs +20/−22
- src/Development/IDE/Types/HscEnvEq.hs +8/−1
- src/Development/IDE/Types/Location.hs +2/−2
- src/Development/IDE/Types/Logger.hs +0/−335
- src/Development/IDE/Types/Options.hs +6/−5
- src/Text/Fuzzy/Parallel.hs +56/−20
- test/exe/AsyncTests.hs +53/−0
- test/exe/BootTests.hs +55/−0
- test/exe/CPPTests.hs +56/−0
- test/exe/ClientSettingsTests.hs +34/−0
- test/exe/CodeLensTests.hs +114/−0
- test/exe/CompletionTests.hs +576/−0
- test/exe/CradleTests.hs +219/−0
- test/exe/DependentFileTest.hs +62/−0
- test/exe/DiagnosticTests.hs +565/−0
- test/exe/ExceptionTests.hs +155/−0
- test/exe/FindDefinitionAndHoverTests.hs +252/−0
- test/exe/FuzzySearch.hs +0/−3
- test/exe/GarbageCollectionTests.hs +94/−0
- test/exe/HaddockTests.hs +90/−0
- test/exe/HieDbRetry.hs +15/−16
- test/exe/HighlightTests.hs +85/−0
- test/exe/IfaceTests.hs +163/−0
- test/exe/InitializeResponseTests.hs +97/−0
- test/exe/LogType.hs +21/−0
- test/exe/Main.hs +99/−3600
- test/exe/NonLspCommandLine.hs +27/−0
- test/exe/OpenCloseTest.hs +18/−0
- test/exe/OutlineTests.hs +189/−0
- test/exe/PluginParsedResultTests.hs +17/−0
- test/exe/PluginSimpleTests.hs +51/−0
- test/exe/PositionMappingTests.hs +199/−0
- test/exe/PreprocessorTests.hs +27/−0
- test/exe/ReferenceTests.hs +199/−0
- test/exe/RootUriTests.hs +26/−0
- test/exe/SafeTests.hs +38/−0
- test/exe/SymlinkTests.hs +27/−0
- test/exe/THTests.hs +194/−0
- test/exe/TestUtils.hs +329/−0
- test/exe/UnitTests.hs +110/−0
- test/exe/WatchedFileTests.hs +83/−0
- test/src/Development/IDE/Test.hs +26/−25
- test/src/Development/IDE/Test/Diagnostic.hs +9/−9
exe/Main.hs view
@@ -22,7 +22,9 @@ import qualified Development.IDE.Monitoring.EKG as EKG import qualified Development.IDE.Monitoring.OpenTelemetry as OpenTelemetry import qualified Development.IDE.Plugin.HLS.GhcIde as GhcIde-import Development.IDE.Types.Logger (Logger (Logger),+import Development.IDE.Types.Options+import GHC.Stack (emptyCallStack)+import Ide.Logger (Logger (Logger), LoggingColumn (DataColumn, PriorityColumn), Pretty (pretty), Priority (Debug, Error, Info),@@ -33,16 +35,14 @@ layoutPretty, makeDefaultStderrRecorder, renderStrict)-import qualified Development.IDE.Types.Logger as Logger-import Development.IDE.Types.Options-import GHC.Stack (emptyCallStack)+import qualified Ide.Logger as Logger import Ide.Plugin.Config (Config (checkParents, checkProject)) import Ide.PluginUtils (pluginDescToIdePlugins) import Ide.Types (PluginDescriptor (pluginNotificationHandlers), defaultPluginDescriptor, mkPluginNotificationHandler)+import Language.LSP.Protocol.Message as LSP import Language.LSP.Server as LSP-import Language.LSP.Types as LSP import Paths_ghcide (version) import qualified System.Directory.Extra as IO import System.Environment (getExecutablePath)@@ -101,7 +101,7 @@ -- This plugin just installs a handler for the `initialized` notification, which then -- picks up the LSP environment and feeds it to our recorders let lspRecorderPlugin = (defaultPluginDescriptor "LSPRecorderCallback")- { pluginNotificationHandlers = mkPluginNotificationHandler LSP.SInitialized $ \_ _ _ _ -> do+ { pluginNotificationHandlers = mkPluginNotificationHandler LSP.SMethod_Initialized $ \_ _ _ _ -> do env <- LSP.getLspEnv liftIO $ (cb1 <> cb2) env }
ghcide.cabal view
@@ -2,7 +2,7 @@ build-type: Simple category: Development name: ghcide-version: 2.0.0.1+version: 2.1.0.0 license: Apache-2.0 license-file: LICENSE author: Digital Asset and Ghcide contributors@@ -65,12 +65,12 @@ haddock-library >= 1.8 && < 1.12, hashable, hie-compat ^>= 0.3.0.0,- hls-plugin-api == 2.0.0.1,+ hls-plugin-api == 2.1.0.0, lens, list-t, hiedb == 0.4.3.*,- lsp-types ^>= 1.6.0.0,- lsp ^>= 1.6.0.0 ,+ lsp-types ^>= 2.0.1.0,+ lsp ^>= 2.1.0.0 , mtl, optparse-applicative, parallel,@@ -78,9 +78,10 @@ prettyprinter >= 1.7, random, regex-tdfa >= 1.3.1.0,+ row-types, text-rope, safe-exceptions,- hls-graph == 2.0.0.1,+ hls-graph == 2.1.0.0, sorted-list, sqlite-simple, stm,@@ -153,6 +154,7 @@ Development.IDE.Core.FileUtils Development.IDE.Core.IdeConfiguration Development.IDE.Core.OfInterest+ Development.IDE.Core.PluginUtils Development.IDE.Core.PositionMapping Development.IDE.Core.Preprocessor Development.IDE.Core.ProgressReporting@@ -196,7 +198,6 @@ Development.IDE.Types.HscEnvEq Development.IDE.Types.KnownTargets Development.IDE.Types.Location- Development.IDE.Types.Logger Development.IDE.Types.Monitoring Development.IDE.Monitoring.OpenTelemetry Development.IDE.Types.Options@@ -345,7 +346,8 @@ hls-plugin-api, lens, list-t,- lsp-test ^>= 0.14,+ lsp-test ^>= 0.15.0.1,+ mtl, monoid-subclasses, network-uri, QuickCheck,@@ -363,6 +365,7 @@ text, text-rope, unordered-containers,+ row-types if impl(ghc < 9.2) build-depends: record-dot-preprocessor,@@ -381,6 +384,39 @@ HieDbRetry Development.IDE.Test Development.IDE.Test.Diagnostic+ ExceptionTests+ -- Tests that have been pulled out of the main file+ BootTests+ CodeLensTests+ CompletionTests+ CPPTests+ CradleTests+ DependentFileTest+ DiagnosticTests+ FindDefinitionAndHoverTests+ HaddockTests+ HighlightTests+ IfaceTests+ InitializeResponseTests+ LogType+ NonLspCommandLine+ OutlineTests+ PluginParsedResultTests+ PluginSimpleTests+ PositionMappingTests+ PreprocessorTests+ RootUriTests+ SafeTests+ SymlinkTests+ TestUtils+ THTests+ UnitTests+ WatchedFileTests+ AsyncTests+ ClientSettingsTests+ ReferenceTests+ GarbageCollectionTests+ OpenCloseTest default-extensions: BangPatterns DeriveFunctor
session-loader/Development/IDE/Session.hs view
@@ -43,6 +43,7 @@ import Data.List import qualified Data.Map.Strict as Map import Data.Maybe+import Data.Proxy import qualified Data.Text as T import Data.Time.Clock import Data.Version@@ -64,13 +65,6 @@ import Development.IDE.Types.HscEnvEq (HscEnvEq, newHscEnvEq, newHscEnvEqPreserveImportPaths) import Development.IDE.Types.Location-import Development.IDE.Types.Logger (Pretty (pretty),- Priority (Debug, Error, Info, Warning),- Recorder, WithPriority,- cmapWithPrio, logWith,- nest,- toCologActionWithPrio,- vcat, viaShow, (<+>)) import Development.IDE.Types.Options import GHC.Check import qualified HIE.Bios as HieBios@@ -78,8 +72,15 @@ import HIE.Bios.Types hiding (Log) import qualified HIE.Bios.Types as HieBios import Hie.Implicit.Cradle (loadImplicitHieCradle)+import Ide.Logger (Pretty (pretty),+ Priority (Debug, Error, Info, Warning),+ Recorder, WithPriority,+ cmapWithPrio, logWith,+ nest,+ toCologActionWithPrio,+ vcat, viaShow, (<+>))+import Language.LSP.Protocol.Message import Language.LSP.Server-import Language.LSP.Types import System.Directory import qualified System.Directory.Extra as IO import System.FilePath@@ -483,7 +484,25 @@ packageSetup (hieYaml, cfp, opts, libDir) = do -- Parse DynFlags for the newly discovered component hscEnv <- emptyHscEnv ideNc libDir- (df, targets) <- evalGhcEnv hscEnv $ setOptions opts (hsc_dflags hscEnv)+ (df', targets) <- evalGhcEnv hscEnv $ setOptions opts (hsc_dflags hscEnv)+ let df =+#if MIN_VERSION_ghc(9,3,0)+ case unitIdString (homeUnitId_ df') of+ -- cabal uses main for the unit id of all executable packages+ -- This makes multi-component sessions confused about what+ -- options to use for that component.+ -- Solution: hash the options and use that as part of the unit id+ -- This works because there won't be any dependencies on the+ -- executable unit.+ "main" ->+ let hash = B.unpack $ B16.encode $ H.finalize $ H.updates H.init (map B.pack $ componentOptions opts)+ hashed_uid = Compat.toUnitId (Compat.stringToUnit ("main-"++hash))+ in setHomeUnitId_ hashed_uid df'+ _ -> df'+#else+ df'+#endif+ let deps = componentDependencies opts ++ maybeToList hieYaml dep_info <- getDependencyInfo deps -- Now lookup to see whether we are combining with an existing HscEnv@@ -498,6 +517,7 @@ -- We will modify the unitId and DynFlags used for -- compilation but these are the true source of -- information.+ new_deps = RawComponentInfo (homeUnitId_ df) df targets cfp opts dep_info : maybe [] snd oldDeps -- Get all the unit-ids for things in this component@@ -632,7 +652,7 @@ lfp <- flip makeRelative cfp <$> getCurrentDirectory when optTesting $ mRunLspT lspEnv $- sendNotification (SCustomMethod "ghcide/cradle/loaded") (toJSON cfp)+ sendNotification (SMethod_CustomMethod (Proxy @"ghcide/cradle/loaded")) (toJSON cfp) -- Display a user friendly progress message here: They probably don't know what a cradle is let progMsg = "Setting up " <> T.pack (takeBaseName (cradleRootDir cradle))@@ -906,7 +926,7 @@ renderCradleError :: NormalizedFilePath -> CradleError -> FileDiagnostic renderCradleError nfp (CradleError _ _ec t) =- ideErrorWithSource (Just "cradle") (Just DsError) nfp (T.unlines (map T.pack t))+ ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) nfp (T.unlines (map T.pack t)) -- See Note [Multi Cradle Dependency Info] type DependencyInfo = Map.Map FilePath (Maybe UTCTime)@@ -1120,4 +1140,4 @@ renderPackageSetupException :: FilePath -> PackageSetupException -> (NormalizedFilePath, ShowDiagnostic, Diagnostic) renderPackageSetupException fp e =- ideErrorWithSource (Just "cradle") (Just DsError) (toNormalizedFilePath' fp) (T.pack $ showPackageSetupException e)+ ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) (toNormalizedFilePath' fp) (T.pack $ showPackageSetupException e)
src/Development/IDE.hs view
@@ -8,9 +8,7 @@ import Development.IDE.Core.Actions as X (getAtPoint, getDefinition,- getTypeDefinition,- useE, useNoFileE,- usesE)+ getTypeDefinition) import Development.IDE.Core.FileExists as X (getFileExists) import Development.IDE.Core.FileStore as X (getFileContents) import Development.IDE.Core.IdeConfiguration as X (IdeConfiguration (..),@@ -55,4 +53,4 @@ hscEnv, hscEnvWithImportPaths) import Development.IDE.Types.Location as X-import Development.IDE.Types.Logger as X+import Ide.Logger as X
src/Development/IDE/Core/Actions.hs view
@@ -6,13 +6,11 @@ , getTypeDefinition , highlightAtPoint , refsAtPoint-, useE-, useNoFileE-, usesE , workspaceSymbols , lookupMod ) where +import Control.Monad.Extra (mapMaybeM) import Control.Monad.Reader import Control.Monad.Trans.Maybe import qualified Data.HashMap.Strict as HM@@ -20,6 +18,7 @@ import qualified Data.Text as T import Data.Tuple.Extra import Development.IDE.Core.OfInterest+import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes import Development.IDE.Core.Service@@ -30,8 +29,10 @@ import Development.IDE.Types.HscEnvEq (hscEnv) import Development.IDE.Types.Location import qualified HieDb-import Language.LSP.Types (DocumentHighlight (..),- SymbolInformation (..))+import Language.LSP.Protocol.Types (DocumentHighlight (..),+ SymbolInformation (..),+ normalizedFilePathToUri,+ uriToNormalizedFilePath) -- | Eventually this will lookup/generate URIs for files in dependencies, but not in the@@ -46,7 +47,7 @@ lookupMod _dbchan _hie_f _mod _uid _boot = MaybeT $ pure Nothing --- IMPORTANT NOTE : make sure all rules `useE`d by these have a "Persistent Stale" rule defined,+-- IMPORTANT NOTE : make sure all rules `useWithStaleFastMT`d by these have a "Persistent Stale" rule defined, -- so we can quickly answer as soon as the IDE is opened -- Even if we don't have persistent information on disk for these rules, the persistent rule -- should just return an empty result@@ -59,50 +60,67 @@ ide <- ask opts <- liftIO $ getIdeOptionsIO ide - (hf, mapping) <- useE GetHieAst file- env <- hscEnv . fst <$> useE GhcSession file- dkMap <- lift $ maybe (DKMap mempty mempty) fst <$> runMaybeT (useE GetDocMap file)+ (hf, mapping) <- useWithStaleFastMT GetHieAst file+ env <- hscEnv . fst <$> useWithStaleFastMT GhcSession file+ dkMap <- lift $ maybe (DKMap mempty mempty) fst <$> runMaybeT (useWithStaleFastMT GetDocMap file) !pos' <- MaybeT (return $ fromCurrentPosition mapping pos)- MaybeT $ pure $ first (toCurrentRange mapping =<<) <$> AtPoint.atPoint opts hf dkMap env pos'+ MaybeT $ liftIO $ fmap (first (toCurrentRange mapping =<<)) <$> AtPoint.atPoint opts hf dkMap env pos' -toCurrentLocations :: PositionMapping -> [Location] -> [Location]-toCurrentLocations mapping = mapMaybe go+-- | For each Loacation, determine if we have the PositionMapping+-- for the correct file. If not, get the correct position mapping+-- and then apply the position mapping to the location.+toCurrentLocations+ :: PositionMapping+ -> NormalizedFilePath+ -> [Location]+ -> IdeAction [Location]+toCurrentLocations mapping file = mapMaybeM go where- go (Location uri range) = Location uri <$> toCurrentRange mapping range---- | useE is useful to implement functions that aren’t rules but need shortcircuiting--- e.g. getDefinition.-useE :: IdeRule k v => k -> NormalizedFilePath -> MaybeT IdeAction (v, PositionMapping)-useE k = MaybeT . useWithStaleFast k--useNoFileE :: IdeRule k v => IdeState -> k -> MaybeT IdeAction v-useNoFileE _ide k = fst <$> useE k emptyFilePath--usesE :: IdeRule k v => k -> [NormalizedFilePath] -> MaybeT IdeAction [(v,PositionMapping)]-usesE k = MaybeT . fmap sequence . mapM (useWithStaleFast k)+ go :: Location -> IdeAction (Maybe Location)+ go (Location uri range) =+ -- The Location we are going to might be in a different+ -- file than the one we are calling gotoDefinition from.+ -- So we check that the location file matches the file+ -- we are in.+ if nUri == normalizedFilePathToUri file+ -- The Location matches the file, so use the PositionMapping+ -- we have.+ then pure $ Location uri <$> toCurrentRange mapping range+ -- The Location does not match the file, so get the correct+ -- PositionMapping and use that instead.+ else do+ otherLocationMapping <- fmap (fmap snd) $ runMaybeT $ do+ otherLocationFile <- MaybeT $ pure $ uriToNormalizedFilePath nUri+ useWithStaleFastMT GetHieAst otherLocationFile+ pure $ Location uri <$> (flip toCurrentRange range =<< otherLocationMapping)+ where+ nUri :: NormalizedUri+ nUri = toNormalizedUri uri -- | Goto Definition. getDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location]) getDefinition file pos = runMaybeT $ do ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask opts <- liftIO $ getIdeOptionsIO ide- (HAR _ hf _ _ _, mapping) <- useE GetHieAst file- (ImportMap imports, _) <- useE GetImportMap file+ (HAR _ hf _ _ _, mapping) <- useWithStaleFastMT GetHieAst file+ (ImportMap imports, _) <- useWithStaleFastMT GetImportMap file !pos' <- MaybeT (pure $ fromCurrentPosition mapping pos)- toCurrentLocations mapping <$> AtPoint.gotoDefinition withHieDb (lookupMod hiedbWriter) opts imports hf pos'+ locations <- AtPoint.gotoDefinition withHieDb (lookupMod hiedbWriter) opts imports hf pos'+ MaybeT $ Just <$> toCurrentLocations mapping file locations getTypeDefinition :: NormalizedFilePath -> Position -> IdeAction (Maybe [Location]) getTypeDefinition file pos = runMaybeT $ do ide@ShakeExtras{ withHieDb, hiedbWriter } <- ask opts <- liftIO $ getIdeOptionsIO ide- (hf, mapping) <- useE GetHieAst file+ (hf, mapping) <- useWithStaleFastMT GetHieAst file !pos' <- MaybeT (return $ fromCurrentPosition mapping pos)- toCurrentLocations mapping <$> AtPoint.gotoTypeDefinition withHieDb (lookupMod hiedbWriter) opts hf pos'+ locations <- AtPoint.gotoTypeDefinition withHieDb (lookupMod hiedbWriter) opts hf pos'+ MaybeT $ Just <$> toCurrentLocations mapping file locations highlightAtPoint :: NormalizedFilePath -> Position -> IdeAction (Maybe [DocumentHighlight]) highlightAtPoint file pos = runMaybeT $ do- (HAR _ hf rf _ _,mapping) <- useE GetHieAst file+ (HAR _ hf rf _ _,mapping) <- useWithStaleFastMT GetHieAst file !pos' <- MaybeT (return $ fromCurrentPosition mapping pos) let toCurrentHighlight (DocumentHighlight range t) = flip DocumentHighlight t <$> toCurrentRange mapping range mapMaybe toCurrentHighlight <$>AtPoint.documentHighlight hf rf pos'
src/Development/IDE/Core/Compile.hs view
@@ -34,6 +34,9 @@ , ml_core_file , coreFileToLinkable , TypecheckHelpers(..)+ , sourceTypecheck+ , sourceParser+ , shareUsages ) where import Control.Monad.IO.Class@@ -64,6 +67,7 @@ import Data.List.Extra import Data.Map (Map) import qualified Data.Map.Strict as Map+import Data.Proxy (Proxy(Proxy)) import qualified Data.Set as Set import Data.Maybe import qualified Data.Text as T@@ -98,8 +102,9 @@ import GHC.Serialized import HieDb import qualified Language.LSP.Server as LSP-import Language.LSP.Types (DiagnosticTag (..))-import qualified Language.LSP.Types as LSP+import Language.LSP.Protocol.Types (DiagnosticTag (..))+import qualified Language.LSP.Protocol.Types as LSP+import qualified Language.LSP.Protocol.Message as LSP import System.Directory import System.FilePath import System.IO.Extra (fixIO, newTempFileWithin)@@ -132,6 +137,7 @@ import qualified GHC as G import GHC.Hs (LEpaComment) import qualified GHC.Types.Error as Error+import Development.IDE.Import.DependencyInformation #endif #if MIN_VERSION_ghc(9,5,0)@@ -139,6 +145,12 @@ import GHC.Core.Lint.Interactive #endif +--Simple constansts to make sure the source is consistently named+sourceTypecheck :: T.Text+sourceTypecheck = "typecheck"+sourceParser :: T.Text+sourceParser = "parser"+ -- | Given a string buffer, return the string (after preprocessing) and the 'ParsedModule'. parseModule :: IdeOptions@@ -182,13 +194,13 @@ case initialized of Left errs -> return (errs, Nothing) Right (modSummary', hsc) -> do- (warnings, etcm) <- withWarnings "typecheck" $ \tweak ->+ (warnings, etcm) <- withWarnings sourceTypecheck $ \tweak -> let session = tweak (hscSetFlags dflags hsc) -- TODO: maybe settings ms_hspp_opts is unnecessary? mod_summary'' = modSummary' { ms_hspp_opts = hsc_dflags session} in- catchSrcErrors (hsc_dflags hsc) "typecheck" $ do+ catchSrcErrors (hsc_dflags hsc) sourceTypecheck $ do tcRnModule session tc_helpers $ demoteIfDefer pm{pm_mod_summary = mod_summary''} let errorPipeline = unDefer . hideDiag dflags . tagDiag diags = map errorPipeline warnings@@ -351,6 +363,10 @@ #if MIN_VERSION_ghc(9,3,0) -- TODO: support backpack nodeKeyToInstalledModule :: NodeKey -> Maybe InstalledModule+ -- We shouldn't get boot files here, but to be safe, never map them to an installed module+ -- because boot files don't have linkables we can load, and we will fail if we try to look+ -- for them+ nodeKeyToInstalledModule (NodeKey_Module (ModNodeKeyWithUid (GWIB mod IsBoot) uid)) = Nothing nodeKeyToInstalledModule (NodeKey_Module (ModNodeKeyWithUid (GWIB mod _) uid)) = Just $ mkModule uid mod nodeKeyToInstalledModule _ = Nothing moduleToNodeKey :: Module -> NodeKey@@ -458,6 +474,8 @@ #endif -- | Mitigation for https://gitlab.haskell.org/ghc/ghc/-/issues/22744+-- Important to do this immediately after reading the unit before+-- anything else has a chance to read `mi_usages` shareUsages :: ModIface -> ModIface shareUsages iface = iface {mi_usages = usages} where usages = map go (mi_usages iface)@@ -611,7 +629,7 @@ source = "compile" catchErrs x = x `catches` [ Handler $ return . (,Nothing) . diagFromGhcException source dflags- , Handler $ return . (,Nothing) . diagFromString source DsError (noSpan "<internal>")+ , Handler $ return . (,Nothing) . diagFromString source DiagnosticSeverity_Error (noSpan "<internal>") . (("Error during " ++ T.unpack source) ++) . show @SomeException ] @@ -741,7 +759,7 @@ upgradeWarningToError :: FileDiagnostic -> FileDiagnostic upgradeWarningToError (nfp, sh, fd) =- (nfp, sh, fd{_severity = Just DsError, _message = warn2err $ _message fd}) where+ (nfp, sh, fd{_severity = Just DiagnosticSeverity_Error, _message = warn2err $ _message fd}) where warn2err :: T.Text -> T.Text warn2err = T.intercalate ": error:" . T.splitOn ": warning:" @@ -780,18 +798,15 @@ tagDiag (w@(Reason warning), (nfp, sh, fd)) #endif | Just tag <- requiresTag warning- = (w, (nfp, sh, fd { _tags = addTag tag (_tags fd) }))+ = (w, (nfp, sh, fd { _tags = Just $ tag : concat (_tags fd) })) where requiresTag :: WarningFlag -> Maybe DiagnosticTag requiresTag Opt_WarnWarningsDeprecations- = Just DtDeprecated+ = Just DiagnosticTag_Deprecated requiresTag wflag -- deprecation was already considered above | wflag `elem` unnecessaryDeprecationWarningFlags- = Just DtUnnecessary+ = Just DiagnosticTag_Unnecessary requiresTag _ = Nothing- addTag :: DiagnosticTag -> Maybe (List DiagnosticTag) -> Maybe (List DiagnosticTag)- addTag t Nothing = Just (List [t])- addTag t (Just (List ts)) = Just (List (t : ts)) -- other diagnostics are left unaffected tagDiag t = t @@ -901,9 +916,10 @@ -- If the hash in the pending list doesn't match the current hash, then skip Just pendingHash -> pendingHash /= hash unless newerScheduled $ do- pre optProgressStyle- withHieDb (\db -> HieDb.addRefsFromLoaded db targetPath (HieDb.RealFile $ fromNormalizedFilePath srcPath) hash hf')- post+ -- Using bracket, so even if an exception happen during withHieDb call,+ -- the `post` (which clean the progress indicator) will still be called.+ bracket_ (pre optProgressStyle) post $+ withHieDb (\db -> HieDb.addRefsFromLoaded db targetPath (HieDb.RealFile $ fromNormalizedFilePath srcPath) hash hf') where mod_location = ms_location mod_summary targetPath = Compat.ml_hie_file mod_location@@ -918,12 +934,13 @@ case lspEnv se of Nothing -> pure Nothing Just env -> LSP.runLspT env $ do- u <- LSP.ProgressTextToken . T.pack . show . hashUnique <$> liftIO Unique.newUnique+ u <- LSP.ProgressToken . LSP.InR . T.pack . show . hashUnique <$> liftIO Unique.newUnique -- TODO: Wait for the progress create response to use the token- _ <- LSP.sendRequest LSP.SWindowWorkDoneProgressCreate (LSP.WorkDoneProgressCreateParams u) (const $ pure ())- LSP.sendNotification LSP.SProgress $ LSP.ProgressParams u $- LSP.Begin $ LSP.WorkDoneProgressBeginParams- { _title = "Indexing"+ _ <- LSP.sendRequest LSP.SMethod_WindowWorkDoneProgressCreate (LSP.WorkDoneProgressCreateParams u) (const $ pure ())+ LSP.sendNotification LSP.SMethod_Progress $ LSP.ProgressParams u $+ toJSON $ LSP.WorkDoneProgressBegin+ { _kind = LSP.AString @"begin"+ , _title = "Indexing" , _cancellable = Nothing , _message = Nothing , _percentage = Nothing@@ -941,22 +958,25 @@ progressPct = floor $ 100 * progressFrac whenJust (lspEnv se) $ \env -> whenJust tok $ \tok -> LSP.runLspT env $- LSP.sendNotification LSP.SProgress $ LSP.ProgressParams tok $- LSP.Report $+ LSP.sendNotification LSP.SMethod_Progress $ LSP.ProgressParams tok $+ toJSON $ case style of- Percentage -> LSP.WorkDoneProgressReportParams- { _cancellable = Nothing+ Percentage -> LSP.WorkDoneProgressReport+ { _kind = LSP.AString @"report"+ , _cancellable = Nothing , _message = Nothing , _percentage = Just progressPct }- Explicit -> LSP.WorkDoneProgressReportParams- { _cancellable = Nothing+ Explicit -> LSP.WorkDoneProgressReport+ { _kind = LSP.AString @"report"+ , _cancellable = Nothing , _message = Just $ T.pack " (" <> T.pack (show done) <> "/" <> T.pack (show $ done + remaining) <> ")..." , _percentage = Nothing }- NoProgress -> LSP.WorkDoneProgressReportParams- { _cancellable = Nothing+ NoProgress -> LSP.WorkDoneProgressReport+ { _kind = LSP.AString @"report"+ , _cancellable = Nothing , _message = Nothing , _percentage = Nothing }@@ -973,15 +993,17 @@ swapTVar indexCompleted 0 whenJust (lspEnv se) $ \env -> LSP.runLspT env $ when (coerce $ ideTesting se) $- LSP.sendNotification (LSP.SCustomMethod "ghcide/reference/ready") $+ LSP.sendNotification (LSP.SMethod_CustomMethod (Proxy @"ghcide/reference/ready")) $ toJSON $ fromNormalizedFilePath srcPath whenJust mdone $ \done -> modifyVar_ indexProgressToken $ \tok -> do whenJust (lspEnv se) $ \env -> LSP.runLspT env $ whenJust tok $ \tok ->- LSP.sendNotification LSP.SProgress $ LSP.ProgressParams tok $- LSP.End $ LSP.WorkDoneProgressEndParams- { _message = Just $ "Finished indexing " <> T.pack (show done) <> " files"+ LSP.sendNotification LSP.SMethod_Progress $ LSP.ProgressParams tok $+ toJSON $+ LSP.WorkDoneProgressEnd+ { _kind = LSP.AString @"end"+ , _message = Just $ "Finished indexing " <> T.pack (show done) <> " files" } -- We are done with the current indexing cycle, so destroy the token pure Nothing@@ -1013,7 +1035,7 @@ handleGenerationErrors dflags source action = action >> return [] `catches` [ Handler $ return . diagFromGhcException source dflags- , Handler $ return . diagFromString source DsError (noSpan "<internal>")+ , Handler $ return . diagFromString source DiagnosticSeverity_Error (noSpan "<internal>") . (("Error during " ++ T.unpack source) ++) . show @SomeException ] @@ -1021,7 +1043,7 @@ handleGenerationErrors' dflags source action = fmap ([],) action `catches` [ Handler $ return . (,Nothing) . diagFromGhcException source dflags- , Handler $ return . (,Nothing) . diagFromString source DsError (noSpan "<internal>")+ , Handler $ return . (,Nothing) . diagFromString source DiagnosticSeverity_Error (noSpan "<internal>") . (("Error during " ++ T.unpack source) ++) . show @SomeException ] @@ -1031,25 +1053,19 @@ -- Add the current ModSummary to the graph, along with the -- HomeModInfo's of all direct dependencies (by induction hypothesis all -- transitive dependencies will be contained in envs)+mergeEnvs :: HscEnv -> ModuleGraph -> ModSummary -> [HomeModInfo] -> [HscEnv] -> IO HscEnv+mergeEnvs env mg ms extraMods envs = do #if MIN_VERSION_ghc(9,3,0)-mergeEnvs :: HscEnv -> (ModSummary, [NodeKey]) -> [HomeModInfo] -> [HscEnv] -> IO HscEnv-mergeEnvs env (ms, deps) extraMods envs = do let im = Compat.installedModule (toUnitId $ moduleUnit $ ms_mod ms) (moduleName (ms_mod ms)) ifr = InstalledFound (ms_location ms) im curFinderCache = Compat.extendInstalledModuleEnv Compat.emptyInstalledModuleEnv im ifr- -- Very important to force this as otherwise the hsc_mod_graph field is not- -- forced and ends up retaining a reference to all the old hsc_envs we have merged to get- -- this new one, which in turn leads to the EPS referencing the HPT.- module_graph_nodes =- nubOrdOn mkNodeKey (ModuleNode deps ms : concatMap (mgModSummaries' . hsc_mod_graph) envs)- newFinderCache <- concatFC curFinderCache (map hsc_FC envs)- liftRnf rwhnf module_graph_nodes `seq` (return $ loadModulesHome extraMods $+ return $! loadModulesHome extraMods $ let newHug = foldl' mergeHUG (hsc_HUG env) (map hsc_HUG envs) in (hscUpdateHUG (const newHug) env){ hsc_FC = newFinderCache,- hsc_mod_graph = mkModuleGraph module_graph_nodes- })+ hsc_mod_graph = mg+ } where mergeHUG (UnitEnvGraph a) (UnitEnvGraph b) = UnitEnvGraph $ Map.unionWith mergeHUE a b@@ -1059,39 +1075,32 @@ combineModules a b | HsSrcFile <- mi_hsc_src (hm_iface a) = a | otherwise = b++ -- Prefer non-boot files over non-boot files+ -- otherwise we can get errors like https://gitlab.haskell.org/ghc/ghc/-/issues/19816+ -- if a boot file shadows over a non-boot file+ combineModuleLocations a@(InstalledFound ml m) b | Just fp <- ml_hs_file ml, not ("boot" `isSuffixOf` fp) = a+ combineModuleLocations _ b = b+ concatFC :: FinderCacheState -> [FinderCache] -> IO FinderCache concatFC cur xs = do fcModules <- mapM (readIORef . fcModuleCache) xs fcFiles <- mapM (readIORef . fcFileCache) xs- fcModules' <- newIORef $! foldl' (plusInstalledModuleEnv const) cur fcModules+ fcModules' <- newIORef $! foldl' (plusInstalledModuleEnv combineModuleLocations) cur fcModules fcFiles' <- newIORef $! Map.unions fcFiles pure $ FinderCache fcModules' fcFiles' #else-mergeEnvs :: HscEnv -> ModSummary -> [HomeModInfo] -> [HscEnv] -> IO HscEnv-mergeEnvs env ms extraMods envs = do prevFinderCache <- concatFC <$> mapM (readIORef . hsc_FC) envs let im = Compat.installedModule (toUnitId $ moduleUnit $ ms_mod ms) (moduleName (ms_mod ms)) ifr = InstalledFound (ms_location ms) im- -- Very important to force this as otherwise the hsc_mod_graph field is not- -- forced and ends up retaining a reference to all the old hsc_envs we have merged to get- -- this new one, which in turn leads to the EPS referencing the HPT.- module_graph_nodes =-#if MIN_VERSION_ghc(9,2,0)- -- We don't do any instantiation for backpack at this point of time, so it is OK to use- -- 'extendModSummaryNoDeps'.- -- This may have to change in the future.- map extendModSummaryNoDeps $-#endif- nubOrdOn ms_mod (ms : concatMap (mgModSummaries . hsc_mod_graph) envs)- newFinderCache <- newIORef $! Compat.extendInstalledModuleEnv prevFinderCache im ifr- liftRnf rwhnf module_graph_nodes `seq` (return $ loadModulesHome extraMods $+ return $! loadModulesHome extraMods $ env{ hsc_HPT = foldMapBy mergeUDFM emptyUDFM hsc_HPT envs, hsc_FC = newFinderCache,- hsc_mod_graph = mkModuleGraph module_graph_nodes- })+ hsc_mod_graph = mg+ } where mergeUDFM = plusUDFM_C combineModules@@ -1248,7 +1257,7 @@ let loc = mkRealSrcLoc (Util.mkFastString filename) 1 1 case unP Compat.parseHeader (initParserState (initParserOpts dflags) contents loc) of PFailedWithErrorMessages msgs ->- throwE $ diagFromErrMsgs "parser" dflags $ msgs dflags+ throwE $ diagFromErrMsgs sourceParser dflags $ msgs dflags POk pst rdr_module -> do let (warns, errs) = renderMessages $ getPsMessages pst dflags @@ -1262,9 +1271,9 @@ -- errors are those from which a parse tree just can't -- be produced. unless (null errs) $- throwE $ diagFromErrMsgs "parser" dflags errs+ throwE $ diagFromErrMsgs sourceParser dflags errs - let warnings = diagFromErrMsgs "parser" dflags warns+ let warnings = diagFromErrMsgs sourceParser dflags warns return (warnings, rdr_module) -- | Given a buffer, flags, and file path, produce a@@ -1281,7 +1290,7 @@ dflags = ms_hspp_opts ms contents = fromJust $ ms_hspp_buf ms case unP Compat.parseModule (initParserState (initParserOpts dflags) contents loc) of- PFailedWithErrorMessages msgs -> throwE $ diagFromErrMsgs "parser" dflags $ msgs dflags+ PFailedWithErrorMessages msgs -> throwE $ diagFromErrMsgs sourceParser dflags $ msgs dflags POk pst rdr_module -> let hpm_annotations = mkApiAnns pst@@ -1291,9 +1300,9 @@ let IdePreprocessedSource preproc_warns errs parsed = customPreprocessor rdr_module unless (null errs) $- throwE $ diagFromStrings "parser" DsError errs+ throwE $ diagFromStrings sourceParser DiagnosticSeverity_Error errs - let preproc_warnings = diagFromStrings "parser" DsWarning preproc_warns+ let preproc_warnings = diagFromStrings sourceParser DiagnosticSeverity_Warning preproc_warns (parsed', msgs) <- liftIO $ applyPluginsParsedResultAction env dflags ms hpm_annotations parsed psMessages let (warns, errs) = renderMessages msgs @@ -1307,7 +1316,7 @@ -- errors are those from which a parse tree just can't -- be produced. unless (null errs) $- throwE $ diagFromErrMsgs "parser" dflags errs+ throwE $ diagFromErrMsgs sourceParser dflags errs -- To get the list of extra source files, we take the list@@ -1342,7 +1351,7 @@ srcs2 <- liftIO $ filterM doesFileExist srcs1 let pm = ParsedModule ms parsed' srcs2 hpm_annotations- warnings = diagFromErrMsgs "parser" dflags warns+ warnings = diagFromErrMsgs sourceParser dflags warns pure (warnings ++ preproc_warnings, pm) loadHieFile :: Compat.NameCacheUpdater -> FilePath -> IO GHC.HieFile@@ -1465,11 +1474,28 @@ | source_version <= dest_version -> SourceUnmodified | otherwise -> SourceModified + old_iface <- case mb_old_iface of+ Just iface -> pure (Just iface)+ Nothing -> do+ let ncu = hsc_NC sessionWithMsDynFlags+ read_dflags = hsc_dflags sessionWithMsDynFlags+#if MIN_VERSION_ghc(9,3,0)+ read_result <- liftIO $ readIface read_dflags ncu mod iface_file+#else+ read_result <- liftIO $ initIfaceCheck (text "readIface") sessionWithMsDynFlags+ $ readIface mod iface_file+#endif+ case read_result of+ Util.Failed{} -> return Nothing+ -- important to call `shareUsages` here before checkOldIface+ -- consults `mi_usages`+ Util.Succeeded iface -> return $ Just (shareUsages iface)+ -- If mb_old_iface is nothing then checkOldIface will load it for us -- given that the source is unmodified (recomp_iface_reqd, mb_checked_iface) #if MIN_VERSION_ghc(9,3,0)- <- liftIO $ checkOldIface sessionWithMsDynFlags ms mb_old_iface >>= \case+ <- liftIO $ checkOldIface sessionWithMsDynFlags ms old_iface >>= \case UpToDateItem x -> pure (UpToDate, Just x) OutOfDateItem reason x -> pure (NeedsRecompile reason, x) #else@@ -1483,15 +1509,14 @@ regenerate linkableNeeded case (mb_checked_iface, recomp_iface_reqd) of- (Just iface', UpToDate) -> do- let iface = shareUsages iface'+ (Just iface, UpToDate) -> do details <- liftIO $ mkDetailsFromIface sessionWithMsDynFlags iface -- parse the runtime dependencies from the annotations let runtime_deps | not (mi_used_th iface) = emptyModuleEnv | otherwise = parseRuntimeDeps (md_anns details)- -- Perform the fine grained recompilation check for TH- maybe_recomp <- checkLinkableDependencies get_linkable_hashes (hsc_mod_graph sessionWithMsDynFlags) runtime_deps+ -- Peform the fine grained recompilation check for TH+ maybe_recomp <- checkLinkableDependencies session get_linkable_hashes runtime_deps case maybe_recomp of Just msg -> do_regenerate msg Nothing@@ -1528,13 +1553,21 @@ -- the runtime dependencies of the module, to check if any of them are out of date -- Hopefully 'runtime_deps' will be empty if the module didn't actually use TH -- See Note [Recompilation avoidance in the presence of TH]-checkLinkableDependencies :: MonadIO m => ([NormalizedFilePath] -> m [BS.ByteString]) -> ModuleGraph -> ModuleEnv BS.ByteString -> m (Maybe RecompileRequired)-checkLinkableDependencies get_linkable_hashes graph runtime_deps = do- let hs_files = mapM go (moduleEnvToList runtime_deps)- go (mod, hash) = do- ms <- mgLookupModule graph mod- let hs = fromJust $ ml_hs_file $ ms_location ms- pure (toNormalizedFilePath' hs, hash)+checkLinkableDependencies :: MonadIO m => HscEnv -> ([NormalizedFilePath] -> m [BS.ByteString]) -> ModuleEnv BS.ByteString -> m (Maybe RecompileRequired)+checkLinkableDependencies hsc_env get_linkable_hashes runtime_deps = do+#if MIN_VERSION_ghc(9,3,0)+ moduleLocs <- liftIO $ readIORef (fcModuleCache $ hsc_FC hsc_env)+#else+ moduleLocs <- liftIO $ readIORef (hsc_FC hsc_env)+#endif+ let go (mod, hash) = do+ ifr <- lookupInstalledModuleEnv moduleLocs $ Compat.installedModule (toUnitId $ moduleUnit mod) (moduleName mod)+ case ifr of+ InstalledFound loc _ -> do+ hs <- ml_hs_file loc+ pure (toNormalizedFilePath' hs,hash)+ _ -> Nothing+ hs_files = mapM go (moduleEnvToList runtime_deps) case hs_files of Nothing -> error "invalid module graph" Just fs -> do
src/Development/IDE/Core/FileExists.hs view
@@ -27,14 +27,14 @@ import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Graph import Development.IDE.Types.Location-import Development.IDE.Types.Logger (Pretty (pretty),- Recorder, WithPriority,- cmapWithPrio) import Development.IDE.Types.Options import qualified Focus+import Ide.Logger (Pretty (pretty),+ Recorder, WithPriority,+ cmapWithPrio) import Ide.Plugin.Config (Config)+import Language.LSP.Protocol.Types import Language.LSP.Server hiding (getVirtualFile)-import Language.LSP.Types import qualified StmContainers.Map as STM import qualified System.Directory as Dir import qualified System.FilePath.Glob as Glob@@ -117,16 +117,16 @@ -- See Note [Invalidating file existence results] -- flush previous values let (fileModifChanges, fileExistChanges) =- partition ((== FcChanged) . snd) changes+ partition ((== FileChangeType_Changed) . snd) changes mapM_ (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges io1 <- recordDirtyKeys (shakeExtras state) GetFileExists $ map fst fileExistChanges io2 <- recordDirtyKeys (shakeExtras state) GetModificationTime $ map fst fileModifChanges return (io1 <> io2) fromChange :: FileChangeType -> Maybe Bool-fromChange FcCreated = Just True-fromChange FcDeleted = Just False-fromChange FcChanged = Nothing+fromChange FileChangeType_Created = Just True+fromChange FileChangeType_Deleted = Just False+fromChange FileChangeType_Changed = Nothing -------------------------------------------------------------------------------------
src/Development/IDE/Core/FileStore.hs view
@@ -54,29 +54,29 @@ #else #endif -import qualified Development.IDE.Types.Logger as L+import qualified Ide.Logger as L +import Data.Aeson (ToJSON (toJSON)) import qualified Data.Binary as B import qualified Data.ByteString.Lazy as LBS import Data.List (foldl') import qualified Data.Text as Text import Development.IDE.Core.IdeConfiguration (isWorkspaceFile) import qualified Development.IDE.Core.Shake as Shake-import Development.IDE.Types.Logger (Pretty (pretty),+import Ide.Logger (Pretty (pretty), Priority (Info), Recorder, WithPriority, cmapWithPrio, logWith, viaShow, (<+>))-import qualified Language.LSP.Server as LSP-import Language.LSP.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions),- FileChangeType (FcChanged),+import Language.LSP.Protocol.Message (toUntypedRegistration)+import qualified Language.LSP.Protocol.Message as LSP+import Language.LSP.Protocol.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions), FileSystemWatcher (..),- WatchKind (..), _watchers)-import qualified Language.LSP.Types as LSP-import qualified Language.LSP.Types.Capabilities as LSP+import qualified Language.LSP.Protocol.Types as LSP+import qualified Language.LSP.Server as LSP import Language.LSP.VFS import System.FilePath import System.IO.Unsafe@@ -162,14 +162,14 @@ -- | Reset the GetModificationTime state of watched files -- Assumes the list does not include any FOIs-resetFileStore :: IdeState -> [(NormalizedFilePath, FileChangeType)] -> IO ()+resetFileStore :: IdeState -> [(NormalizedFilePath, LSP.FileChangeType)] -> IO () resetFileStore ideState changes = mask $ \_ -> do -- we record FOIs document versions in all the stored values -- so NEVER reset FOIs to avoid losing their versions -- FOI filtering is done by the caller (LSP Notification handler) forM_ changes $ \(nfp, c) -> do case c of- FcChanged+ LSP.FileChangeType_Changed -- already checked elsewhere | not $ HM.member nfp fois -> atomically $ deleteValue (shakeExtras ideState) GetModificationTime nfp@@ -268,26 +268,27 @@ if watchSupported then do let- regParams = LSP.RegistrationParams (List [LSP.SomeRegistration registration])+ regParams = LSP.RegistrationParams [toUntypedRegistration registration] -- The registration ID is arbitrary and is only used in case we want to deregister (which we won't). -- We could also use something like a random UUID, as some other servers do, but this works for -- our purposes.- registration = LSP.Registration "globalFileWatches"- LSP.SWorkspaceDidChangeWatchedFiles- regOptions+ registration = LSP.TRegistration { _id ="globalFileWatches"+ , _method = LSP.SMethod_WorkspaceDidChangeWatchedFiles+ , _registerOptions = Just $ regOptions} regOptions =- DidChangeWatchedFilesRegistrationOptions { _watchers = List watchers }+ DidChangeWatchedFilesRegistrationOptions { _watchers = watchers } -- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind- watchKind = WatchKind { _watchCreate = True, _watchChange = True, _watchDelete = True}+ -- WatchKind_Custom 7 is for create, change, and delete+ watchKind = LSP.WatchKind_Custom 7 -- See Note [Which files should we watch?] for an explanation of why the pattern is the way that it is -- The patterns will be something like "**/.hs", i.e. "any number of directory segments, -- followed by a file with an extension 'hs'. watcher glob = FileSystemWatcher { _globPattern = glob, _kind = Just watchKind } -- We use multiple watchers instead of one using '{}' because lsp-test doesn't -- support that: https://github.com/bubba/lsp-test/issues/77- watchers = [ watcher (Text.pack glob) | glob <- globs ]+ watchers = [ watcher (LSP.GlobPattern (LSP.InL (LSP.Pattern (Text.pack glob)))) | glob <- globs ] - void $ LSP.sendRequest LSP.SClientRegisterCapability regParams (const $ pure ()) -- TODO handle response+ void $ LSP.sendRequest LSP.SMethod_ClientRegisterCapability regParams (const $ pure ()) -- TODO handle response return True else return False @@ -311,7 +312,7 @@ atomicModifyIORef' filePathMap $ \km -> let new_key = HashMap.lookup k km in case new_key of- Just v -> (km, v)+ Just v -> (km, v) Nothing -> (HashMap.insert k k km, k) {-# NOINLINE shareFilePath #-}- +
src/Development/IDE/Core/IdeConfiguration.hs view
@@ -13,6 +13,7 @@ where import Control.Concurrent.Strict+import Control.Lens ((^.)) import Control.Monad import Control.Monad.IO.Class import Data.Aeson.Types (Value)@@ -22,7 +23,7 @@ import Development.IDE.Core.Shake import Development.IDE.Graph import Development.IDE.Types.Location-import Language.LSP.Types+import Language.LSP.Protocol.Types import System.FilePath (isRelative) -- | Lsp client relevant configuration details@@ -49,15 +50,15 @@ IdeConfiguration {..} where workspaceFolders =- foldMap (singleton . toNormalizedUri) _rootUri+ foldMap (singleton . toNormalizedUri) (nullToMaybe _rootUri) <> (foldMap . foldMap) (singleton . parseWorkspaceFolder)- _workspaceFolders+ (nullToMaybe =<< _workspaceFolders) clientSettings = hashed _initializationOptions parseWorkspaceFolder :: WorkspaceFolder -> NormalizedUri parseWorkspaceFolder WorkspaceFolder{_uri} =- toNormalizedUri (Uri _uri)+ toNormalizedUri _uri modifyWorkspaceFolders :: IdeState -> (HashSet NormalizedUri -> HashSet NormalizedUri) -> IO ()
src/Development/IDE/Core/OfInterest.hs view
@@ -24,6 +24,7 @@ import Control.Monad.IO.Class import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap+import Data.Proxy import qualified Data.Text as T import Development.IDE.Graph @@ -39,14 +40,15 @@ import Development.IDE.Plugin.Completions.Types import Development.IDE.Types.Exports import Development.IDE.Types.Location-import Development.IDE.Types.Logger (Pretty (pretty),+import Development.IDE.Types.Options (IdeTesting (..))+import GHC.TypeLits (KnownSymbol)+import Ide.Logger (Pretty (pretty), Recorder, WithPriority, cmapWithPrio, logDebug)-import Development.IDE.Types.Options (IdeTesting (..))+import qualified Language.LSP.Protocol.Message as LSP import qualified Language.LSP.Server as LSP-import qualified Language.LSP.Types as LSP data Log = LogShake Shake.Log deriving Show@@ -130,12 +132,13 @@ kick = do files <- HashMap.keys <$> getFilesOfInterestUntracked ShakeExtras{exportsMap, ideTesting = IdeTesting testing, lspEnv, progress} <- getShakeExtras- let signal msg = when testing $ liftIO $+ let signal :: KnownSymbol s => Proxy s -> Action ()+ signal msg = when testing $ liftIO $ mRunLspT lspEnv $- LSP.sendNotification (LSP.SCustomMethod msg) $+ LSP.sendNotification (LSP.SMethod_CustomMethod msg) $ toJSON $ map fromNormalizedFilePath files - signal "kick/start"+ signal (Proxy @"kick/start") liftIO $ progressUpdate progress KickStarted -- Update the exports map@@ -155,4 +158,4 @@ void garbageCollectDirtyKeys liftIO $ writeVar var False - signal "kick/done"+ signal (Proxy @"kick/done")
+ src/Development/IDE/Core/PluginUtils.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE GADTs #-}+module Development.IDE.Core.PluginUtils where++import Control.Monad.Extra+import Control.Monad.IO.Class+import Control.Monad.Reader (runReaderT)+import Control.Monad.Trans.Except+import Control.Monad.Trans.Maybe+import Data.Functor.Identity+import qualified Data.Text as T+import Development.IDE.Core.PositionMapping+import Development.IDE.Core.Shake (IdeAction, IdeRule,+ IdeState (shakeExtras),+ mkDelayedAction,+ shakeEnqueue)+import qualified Development.IDE.Core.Shake as Shake+import Development.IDE.GHC.Orphans ()+import Development.IDE.Graph hiding (ShakeValue)+import Development.IDE.Types.Location (NormalizedFilePath)+import qualified Development.IDE.Types.Location as Location+import qualified Ide.Logger as Logger+import Ide.Plugin.Error+import qualified Language.LSP.Protocol.Types as LSP++-- ----------------------------------------------------------------------------+-- Action wrappers+-- ----------------------------------------------------------------------------++-- |ExceptT version of `runAction`, takes a ExceptT Action+runActionE :: MonadIO m => String -> IdeState -> ExceptT e Action a -> ExceptT e m a+runActionE herald ide act =+ mapExceptT liftIO . ExceptT $+ join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug $ runExceptT act)++-- |MaybeT version of `runAction`, takes a MaybeT Action+runActionMT :: MonadIO m => String -> IdeState -> MaybeT Action a -> MaybeT m a+runActionMT herald ide act =+ mapMaybeT liftIO . MaybeT $+ join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug $ runMaybeT act)++-- |ExceptT version of `use` that throws a PluginRuleFailed upon failure+useE :: IdeRule k v => k -> NormalizedFilePath -> ExceptT PluginError Action v+useE k = maybeToExceptT (PluginRuleFailed (T.pack $ show k)) . useMT k++-- |MaybeT version of `use`+useMT :: IdeRule k v => k -> NormalizedFilePath -> MaybeT Action v+useMT k = MaybeT . Shake.use k++-- |ExceptT version of `uses` that throws a PluginRuleFailed upon failure+usesE :: (Traversable f, IdeRule k v) => k -> f NormalizedFilePath -> ExceptT PluginError Action (f v)+usesE k = maybeToExceptT (PluginRuleFailed (T.pack $ show k)) . usesMT k++-- |MaybeT version of `uses`+usesMT :: (Traversable f, IdeRule k v) => k -> f NormalizedFilePath -> MaybeT Action (f v)+usesMT k xs = MaybeT $ sequence <$> Shake.uses k xs++-- |ExceptT version of `useWithStale` that throws a PluginRuleFailed upon+-- failure+useWithStaleE :: IdeRule k v+ => k -> NormalizedFilePath -> ExceptT PluginError Action (v, PositionMapping)+useWithStaleE key = maybeToExceptT (PluginRuleFailed (T.pack $ show key)) . useWithStaleMT key++-- |MaybeT version of `useWithStale`+useWithStaleMT :: IdeRule k v+ => k -> NormalizedFilePath -> MaybeT Action (v, PositionMapping)+useWithStaleMT key file = MaybeT $ runIdentity <$> Shake.usesWithStale key (Identity file)++-- ----------------------------------------------------------------------------+-- IdeAction wrappers+-- ----------------------------------------------------------------------------++-- |ExceptT version of `runIdeAction`, takes a ExceptT IdeAction+runIdeActionE :: MonadIO m => String -> Shake.ShakeExtras -> ExceptT e IdeAction a -> ExceptT e m a+runIdeActionE _herald s i = ExceptT $ liftIO $ runReaderT (Shake.runIdeActionT $ runExceptT i) s++-- |MaybeT version of `runIdeAction`, takes a MaybeT IdeAction+runIdeActionMT :: MonadIO m => String -> Shake.ShakeExtras -> MaybeT IdeAction a -> MaybeT m a+runIdeActionMT _herald s i = MaybeT $ liftIO $ runReaderT (Shake.runIdeActionT $ runMaybeT i) s++-- |ExceptT version of `useWithStaleFast` that throws a PluginRuleFailed upon+-- failure+useWithStaleFastE :: IdeRule k v => k -> NormalizedFilePath -> ExceptT PluginError IdeAction (v, PositionMapping)+useWithStaleFastE k = maybeToExceptT (PluginRuleFailed (T.pack $ show k)) . useWithStaleFastMT k++-- |MaybeT version of `useWithStaleFast`+useWithStaleFastMT :: IdeRule k v => k -> NormalizedFilePath -> MaybeT IdeAction (v, PositionMapping)+useWithStaleFastMT k = MaybeT . Shake.useWithStaleFast k++-- ----------------------------------------------------------------------------+-- Location wrappers+-- ----------------------------------------------------------------------------++-- |ExceptT version of `uriToFilePath` that throws a PluginInvalidParams upon+-- failure+uriToFilePathE :: Monad m => LSP.Uri -> ExceptT PluginError m FilePath+uriToFilePathE uri = maybeToExceptT (PluginInvalidParams (T.pack $ "uriToFilePath' failed. Uri:" <> show uri)) $ uriToFilePathMT uri++-- |MaybeT version of `uriToFilePath`+uriToFilePathMT :: Monad m => LSP.Uri -> MaybeT m FilePath+uriToFilePathMT = MaybeT . pure . Location.uriToFilePath'++-- ----------------------------------------------------------------------------+-- PositionMapping wrappers+-- ----------------------------------------------------------------------------++-- |ExceptT version of `toCurrentPosition` that throws a PluginInvalidUserState+-- upon failure+toCurrentPositionE :: Monad m => PositionMapping -> LSP.Position -> ExceptT PluginError m LSP.Position+toCurrentPositionE mapping = maybeToExceptT (PluginInvalidUserState "toCurrentPosition"). toCurrentPositionMT mapping++-- |MaybeT version of `toCurrentPosition`+toCurrentPositionMT :: Monad m => PositionMapping -> LSP.Position -> MaybeT m LSP.Position+toCurrentPositionMT mapping = MaybeT . pure . toCurrentPosition mapping++-- |ExceptT version of `fromCurrentPosition` that throws a+-- PluginInvalidUserState upon failure+fromCurrentPositionE :: Monad m => PositionMapping -> LSP.Position -> ExceptT PluginError m LSP.Position+fromCurrentPositionE mapping = maybeToExceptT (PluginInvalidUserState "fromCurrentPosition") . fromCurrentPositionMT mapping++-- |MaybeT version of `fromCurrentPosition`+fromCurrentPositionMT :: Monad m => PositionMapping -> LSP.Position -> MaybeT m LSP.Position+fromCurrentPositionMT mapping = MaybeT . pure . fromCurrentPosition mapping++-- |ExceptT version of `toCurrentRange` that throws a PluginInvalidUserState+-- upon failure+toCurrentRangeE :: Monad m => PositionMapping -> LSP.Range -> ExceptT PluginError m LSP.Range+toCurrentRangeE mapping = maybeToExceptT (PluginInvalidUserState "toCurrentRange") . toCurrentRangeMT mapping++-- |MaybeT version of `toCurrentRange`+toCurrentRangeMT :: Monad m => PositionMapping -> LSP.Range -> MaybeT m LSP.Range+toCurrentRangeMT mapping = MaybeT . pure . toCurrentRange mapping++-- |ExceptT version of `fromCurrentRange` that throws a PluginInvalidUserState+-- upon failure+fromCurrentRangeE :: Monad m => PositionMapping -> LSP.Range -> ExceptT PluginError m LSP.Range+fromCurrentRangeE mapping = maybeToExceptT (PluginInvalidUserState "fromCurrentRange") . fromCurrentRangeMT mapping++-- |MaybeT version of `fromCurrentRange`+fromCurrentRangeMT :: Monad m => PositionMapping -> LSP.Range -> MaybeT m LSP.Range+fromCurrentRangeMT mapping = MaybeT . pure . fromCurrentRange mapping
src/Development/IDE/Core/PositionMapping.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedLabels #-} -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 module Development.IDE.Core.PositionMapping@@ -28,11 +29,13 @@ import Data.Algorithm.Diff import Data.Bifunctor import Data.List-import qualified Data.Text as T-import qualified Data.Vector.Unboxed as V-import Language.LSP.Types (Position (Position), Range (Range),- TextDocumentContentChangeEvent (TextDocumentContentChangeEvent),- UInt)+import Data.Row+import qualified Data.Text as T+import qualified Data.Vector.Unboxed as V+import Language.LSP.Protocol.Types (Position (Position),+ Range (Range),+ TextDocumentContentChangeEvent (TextDocumentContentChangeEvent),+ UInt, type (|?) (InL)) -- | Either an exact position, or the range of text that was substituted data PositionResult a@@ -120,10 +123,12 @@ addDelta :: PositionDelta -> PositionMapping -> PositionMapping addDelta delta (PositionMapping pm) = PositionMapping (composeDelta delta pm) +-- TODO: We currently ignore the right hand side (if there is only text), as+-- that was what was done with lsp* 1.6 packages applyChange :: PositionDelta -> TextDocumentContentChangeEvent -> PositionDelta-applyChange PositionDelta{..} (TextDocumentContentChangeEvent (Just r) _ t) = PositionDelta- { toDelta = toCurrent r t <=< toDelta- , fromDelta = fromDelta <=< fromCurrent r t+applyChange PositionDelta{..} (TextDocumentContentChangeEvent (InL x)) = PositionDelta+ { toDelta = toCurrent (x .! #range) (x .! #text) <=< toDelta+ , fromDelta = fromDelta <=< fromCurrent (x .! #range) (x .! #text) } applyChange posMapping _ = posMapping
src/Development/IDE/Core/Preprocessor.hs view
@@ -8,9 +8,9 @@ import Development.IDE.GHC.Compat import qualified Development.IDE.GHC.Compat.Util as Util-import qualified Development.IDE.GHC.Util as Util import Development.IDE.GHC.CPP import Development.IDE.GHC.Orphans ()+import qualified Development.IDE.GHC.Util as Util import Control.DeepSeq (NFData (rnf)) import Control.Exception (evaluate)@@ -133,7 +133,9 @@ _source = Just "CPP", _message = T.unlines $ cdMessage d, _relatedInformation = Nothing,- _tags = Nothing+ _tags = Nothing,+ _codeDescription = Nothing,+ _data_ = Nothing }
src/Development/IDE/Core/ProgressReporting.hs view
@@ -21,6 +21,7 @@ import Control.Monad.Extra import Control.Monad.IO.Class import Control.Monad.Trans.Class (lift)+import Data.Aeson (ToJSON (toJSON)) import Data.Foldable (for_) import Data.Functor (($>)) import qualified Data.Text as T@@ -30,9 +31,10 @@ import Development.IDE.Types.Location import Development.IDE.Types.Options import qualified Focus+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types+import qualified Language.LSP.Protocol.Types as LSP import qualified Language.LSP.Server as LSP-import Language.LSP.Types-import qualified Language.LSP.Types as LSP import qualified StmContainers.Map as STM import System.Time.Extra import UnliftIO.Exception (bracket_)@@ -125,30 +127,32 @@ -- first sleep a bit, so we only show progress messages if it's going to take -- a "noticable amount of time" (we often expect a thread kill to arrive before the sleep finishes) liftIO $ sleep before- u <- ProgressTextToken . T.pack . show . hashUnique <$> liftIO newUnique+ u <- ProgressToken . InR . T.pack . show . hashUnique <$> liftIO newUnique b <- liftIO newBarrier- void $ LSP.runLspT lspEnv $ LSP.sendRequest LSP.SWindowWorkDoneProgressCreate+ void $ LSP.runLspT lspEnv $ LSP.sendRequest SMethod_WindowWorkDoneProgressCreate LSP.WorkDoneProgressCreateParams { _token = u } $ liftIO . signalBarrier b liftIO $ async $ do ready <- waitBarrier b LSP.runLspT lspEnv $ for_ ready $ const $ bracket_ (start u) (stop u) (loop u 0) where- start id = LSP.sendNotification LSP.SProgress $+ start id = LSP.sendNotification SMethod_Progress $ LSP.ProgressParams { _token = id- , _value = LSP.Begin $ WorkDoneProgressBeginParams- { _title = "Processing"+ , _value = toJSON $ WorkDoneProgressBegin+ { _kind = AString @"begin"+ , _title = "Processing" , _cancellable = Nothing , _message = Nothing , _percentage = Nothing } }- stop id = LSP.sendNotification LSP.SProgress+ stop id = LSP.sendNotification SMethod_Progress LSP.ProgressParams { _token = id- , _value = LSP.End WorkDoneProgressEndParams- { _message = Nothing+ , _value = toJSON $ WorkDoneProgressEnd+ { _kind = AString @"end"+ , _message = Nothing } } loop _ _ | optProgressStyle == NoProgress =@@ -164,17 +168,19 @@ nextPct :: UInt nextPct = floor $ 100 * nextFrac when (nextPct /= prevPct) $- LSP.sendNotification LSP.SProgress $+ LSP.sendNotification SMethod_Progress $ LSP.ProgressParams { _token = id- , _value = LSP.Report $ case optProgressStyle of- Explicit -> LSP.WorkDoneProgressReportParams- { _cancellable = Nothing+ , _value = case optProgressStyle of+ Explicit -> toJSON $ WorkDoneProgressReport+ { _kind = AString @"report"+ , _cancellable = Nothing , _message = Just $ T.pack $ show done <> "/" <> show todo , _percentage = Nothing }- Percentage -> LSP.WorkDoneProgressReportParams- { _cancellable = Nothing+ Percentage -> toJSON $ WorkDoneProgressReport+ { _kind = AString @"report"+ , _cancellable = Nothing , _message = Nothing , _percentage = Just nextPct }
src/Development/IDE/Core/RuleTypes.hs view
@@ -42,7 +42,7 @@ import Development.IDE.Spans.LocalBindings import Development.IDE.Types.Diagnostics import GHC.Serialized (Serialized)-import Language.LSP.Types (Int32,+import Language.LSP.Protocol.Types (Int32, NormalizedFilePath) data LinkableType = ObjectLinkable | BCOLinkable@@ -69,11 +69,6 @@ -- all comments included using Opt_KeepRawTokenStream type instance RuleResult GetParsedModuleWithComments = ParsedModule --- | The dependency information produced by following the imports recursively.--- This rule will succeed even if there is an error, e.g., a module could not be located,--- a module could not be parsed or an import cycle.-type instance RuleResult GetDependencyInformation = DependencyInformation- type instance RuleResult GetModuleGraph = DependencyInformation data GetKnownTargets = GetKnownTargets@@ -262,8 +257,8 @@ -- | Resolve the imports in a module to the file path of a module in the same package type instance RuleResult GetLocatedImports = [(Located ModuleName, Maybe ArtifactsLocation)] --- | This rule is used to report import cycles. It depends on GetDependencyInformation.--- We cannot report the cycles directly from GetDependencyInformation since+-- | This rule is used to report import cycles. It depends on GetModuleGraph.+-- We cannot report the cycles directly from GetModuleGraph since -- we can only report diagnostics for the current file. type instance RuleResult ReportImportCycles = () @@ -400,11 +395,6 @@ deriving (Eq, Show, Typeable, Generic) instance Hashable NeedsCompilation instance NFData NeedsCompilation--data GetDependencyInformation = GetDependencyInformation- deriving (Eq, Show, Typeable, Generic)-instance Hashable GetDependencyInformation-instance NFData GetDependencyInformation data GetModuleGraph = GetModuleGraph deriving (Eq, Show, Typeable, Generic)
src/Development/IDE/Core/Rules.hs view
@@ -23,7 +23,6 @@ defineEarlyCutOffNoFile, mainRule, RulesConfig(..),- getDependencies, getParsedModule, getParsedModuleWithComments, getClientConfigAction,@@ -34,7 +33,6 @@ getParsedModuleRule, getParsedModuleWithCommentsRule, getLocatedImportsRule,- getDependencyInformationRule, reportImportCyclesRule, typeCheckRule, getDocMapRule,@@ -68,6 +66,7 @@ import Control.Concurrent.Strict import Control.DeepSeq import Control.Exception.Safe+import Control.Exception (evaluate) import Control.Monad.Extra import Control.Monad.Reader import Control.Monad.State@@ -90,8 +89,10 @@ import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IntMap import Data.List+import Data.List.Extra (nubOrdOn) import qualified Data.Map as M import Data.Maybe+import Data.Proxy import qualified Data.Text.Utf16.Rope as Rope import qualified Data.Set as Set import qualified Data.Text as T@@ -135,7 +136,8 @@ import qualified HieDb import Ide.Plugin.Config import qualified Language.LSP.Server as LSP-import Language.LSP.Types (SMethod (SCustomMethod, SWindowShowMessage), ShowMessageParams (ShowMessageParams), MessageType (MtInfo))+import Language.LSP.Protocol.Types (ShowMessageParams (ShowMessageParams), MessageType (MessageType_Info))+import Language.LSP.Protocol.Message (SMethod (SMethod_CustomMethod, SMethod_WindowShowMessage)) import Language.LSP.VFS import System.Directory (makeAbsolute, doesFileExist) import Data.Default (def, Default)@@ -151,13 +153,14 @@ import Language.LSP.Server (LspT) import System.Info.Extra (isWindows) import HIE.Bios.Ghc.Gap (hostIsDynamic)-import Development.IDE.Types.Logger (Recorder, logWith, cmapWithPrio, WithPriority, Pretty (pretty), (<+>), nest, vcat)+import Ide.Logger (Recorder, logWith, cmapWithPrio, WithPriority, Pretty (pretty), (<+>), nest, vcat) import qualified Development.IDE.Core.Shake as Shake-import qualified Development.IDE.Types.Logger as Logger+import qualified Ide.Logger as Logger import qualified Development.IDE.Types.Shake as Shake import Development.IDE.GHC.CoreFile import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Control.Monad.IO.Unlift+import qualified Data.IntMap as IM #if MIN_VERSION_ghc(9,3,0) import GHC.Unit.Module.Graph import GHC.Unit.Env@@ -165,6 +168,8 @@ #if MIN_VERSION_ghc(9,5,0) import GHC.Unit.Home.ModInfo #endif+import GHC (mgModSummaries)+import GHC.Fingerprint data Log = LogShake Shake.Log@@ -210,12 +215,6 @@ ------------------------------------------------------------ -- Exposed API --------------------------------------------------------------- | Get all transitive file dependencies of a given module.--- Does not include the file itself.-getDependencies :: NormalizedFilePath -> Action (Maybe [NormalizedFilePath])-getDependencies file =- fmap transitiveModuleDeps . (`transitiveDeps` file) <$> use_ GetDependencyInformation file- getSourceFileSource :: NormalizedFilePath -> Action BS.ByteString getSourceFileSource nfp = do (_, msource) <- getFileContents nfp@@ -314,7 +313,7 @@ -- Ignore Haddock errors that are in both. Demote Haddock-only errors to warnings. mergeParseErrorsHaddock :: [FileDiagnostic] -> [FileDiagnostic] -> [FileDiagnostic] mergeParseErrorsHaddock normal haddock = normal ++- [ (a,b,c{_severity = Just DsWarning, _message = fixMessage $ _message c})+ [ (a,b,c{_severity = Just DiagnosticSeverity_Warning, _message = fixMessage $ _message c}) | (a,b,c) <- haddock, Diag._range c `Set.notMember` locations] where locations = Set.fromList $ map (Diag._range . thd3) normal@@ -420,17 +419,17 @@ execRawDepM :: Monad m => StateT (RawDependencyInformation, IntMap a1) m a2 -> m (RawDependencyInformation, IntMap a1) execRawDepM act = execStateT act- ( RawDependencyInformation IntMap.empty emptyPathIdMap IntMap.empty IntMap.empty+ ( RawDependencyInformation IntMap.empty emptyPathIdMap IntMap.empty , IntMap.empty ) -- | Given a target file path, construct the raw dependency results by following -- imports recursively.-rawDependencyInformation :: [NormalizedFilePath] -> Action RawDependencyInformation+rawDependencyInformation :: [NormalizedFilePath] -> Action (RawDependencyInformation, BootIdMap) rawDependencyInformation fs = do (rdi, ss) <- execRawDepM (goPlural fs) let bm = IntMap.foldrWithKey (updateBootMap rdi) IntMap.empty ss- return (rdi { rawBootMap = bm })+ return (rdi, bm) where goPlural ff = do mss <- lift $ (fmap.fmap) msrModSummary <$> uses GetModSummaryWithoutTimestamps ff@@ -449,9 +448,9 @@ fId <- getFreshFid al -- Record this module and its location whenJust msum $ \ms ->- modifyRawDepInfo (\rd -> rd { rawModuleNameMap = IntMap.insert (getFilePathId fId)- (ShowableModuleName (moduleName $ ms_mod ms))- (rawModuleNameMap rd)})+ modifyRawDepInfo (\rd -> rd { rawModuleMap = IntMap.insert (getFilePathId fId)+ (ShowableModule $ ms_mod ms)+ (rawModuleMap rd)}) -- Adding an edge to the bootmap so we can make sure to -- insert boot nodes before the real files. addBootMap al fId@@ -523,38 +522,37 @@ dropBootSuffix :: FilePath -> FilePath dropBootSuffix hs_src = reverse . drop (length @[] "-boot") . reverse $ hs_src -getDependencyInformationRule :: Recorder (WithPriority Log) -> Rules ()-getDependencyInformationRule recorder =- define (cmapWithPrio LogShake recorder) $ \GetDependencyInformation file -> do- rawDepInfo <- rawDependencyInformation [file]- pure ([], Just $ processDependencyInformation rawDepInfo)- reportImportCyclesRule :: Recorder (WithPriority Log) -> Rules () reportImportCyclesRule recorder =- define (cmapWithPrio LogShake recorder) $ \ReportImportCycles file -> fmap (\errs -> if null errs then ([], Just ()) else (errs, Nothing)) $ do- DependencyInformation{..} <- use_ GetDependencyInformation file- let fileId = pathToId depPathIdMap file- case IntMap.lookup (getFilePathId fileId) depErrorNodes of- Nothing -> pure []- Just errs -> do- let cycles = mapMaybe (cycleErrorInFile fileId) (toList errs)- -- Convert cycles of files into cycles of module names- forM cycles $ \(imp, files) -> do- modNames <- forM files $ \fileId -> do- let file = idToPath depPathIdMap fileId- getModuleName file- pure $ toDiag imp $ sort modNames+ defineEarlyCutoff (cmapWithPrio LogShake recorder) $ Rule $ \ReportImportCycles file -> fmap (\errs -> if null errs then (Just "1",([], Just ())) else (Nothing, (errs, Nothing))) $ do+ DependencyInformation{..} <- useNoFile_ GetModuleGraph+ case pathToId depPathIdMap file of+ -- The header of the file does not parse, so it can't be part of any import cycles.+ Nothing -> pure []+ Just fileId ->+ case IntMap.lookup (getFilePathId fileId) depErrorNodes of+ Nothing -> pure []+ Just errs -> do+ let cycles = mapMaybe (cycleErrorInFile fileId) (toList errs)+ -- Convert cycles of files into cycles of module names+ forM cycles $ \(imp, files) -> do+ modNames <- forM files $ \fileId -> do+ let file = idToPath depPathIdMap fileId+ getModuleName file+ pure $ toDiag imp $ sort modNames where cycleErrorInFile f (PartOfCycle imp fs) | f `elem` fs = Just (imp, fs) cycleErrorInFile _ _ = Nothing toDiag imp mods = (fp , ShowDiag , ) $ Diagnostic { _range = rng- , _severity = Just DsError+ , _severity = Just DiagnosticSeverity_Error , _source = Just "Import cycle detection" , _message = "Cyclic module dependency between " <> showCycle mods , _code = Nothing , _relatedInformation = Nothing , _tags = Nothing+ , _codeDescription = Nothing+ , _data_ = Nothing } where rng = fromMaybe noRange $ srcSpanToRange (getLoc imp) fp = toNormalizedFilePath' $ fromMaybe noFilePath $ srcSpanToFilename (getLoc imp)@@ -591,7 +589,7 @@ diagsWrite <- case isFoi of IsFOI Modified{firstOpen = False} -> do when (coerce $ ideTesting se) $ liftIO $ mRunLspT (lspEnv se) $- LSP.sendNotification (SCustomMethod "ghcide/reference/ready") $+ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/reference/ready")) $ toJSON $ fromNormalizedFilePath f pure [] _ | Just asts <- masts -> do@@ -677,11 +675,38 @@ pure (LBS.toStrict $ B.encode $ hash fs, unhashed fs) getModuleGraphRule :: Recorder (WithPriority Log) -> Rules ()-getModuleGraphRule recorder = defineNoFile (cmapWithPrio LogShake recorder) $ \GetModuleGraph -> do+getModuleGraphRule recorder = defineEarlyCutOffNoFile (cmapWithPrio LogShake recorder) $ \GetModuleGraph -> do fs <- toKnownFiles <$> useNoFile_ GetKnownTargets- rawDepInfo <- rawDependencyInformation (HashSet.toList fs)- pure $ processDependencyInformation rawDepInfo+ dependencyInfoForFiles (HashSet.toList fs) +dependencyInfoForFiles :: [NormalizedFilePath] -> Action (BS.ByteString, DependencyInformation)+dependencyInfoForFiles fs = do+ (rawDepInfo, bm) <- rawDependencyInformation fs+ let (all_fs, _all_ids) = unzip $ HM.toList $ pathToIdMap $ rawPathIdMap rawDepInfo+ msrs <- uses GetModSummaryWithoutTimestamps all_fs+ let mss = map (fmap msrModSummary) msrs+#if MIN_VERSION_ghc(9,3,0)+ let deps = map (\i -> IM.lookup (getFilePathId i) (rawImports rawDepInfo)) _all_ids+ nodeKeys = IM.fromList $ catMaybes $ zipWith (\fi mms -> (getFilePathId fi,) . NodeKey_Module . msKey <$> mms) _all_ids mss+ mns = catMaybes $ zipWith go mss deps+ go (Just ms) (Just (Right (ModuleImports xs))) = Just $ ModuleNode this_dep_keys ms+ where this_dep_ids = mapMaybe snd xs+ this_dep_keys = mapMaybe (\fi -> IM.lookup (getFilePathId fi) nodeKeys) this_dep_ids+ go (Just ms) _ = Just $ ModuleNode [] ms+ go _ _ = Nothing+ mg = mkModuleGraph mns+#else+ let mg = mkModuleGraph $+#if MIN_VERSION_ghc(9,2,0)+ -- We don't do any instantiation for backpack at this point of time, so it is OK to use+ -- 'extendModSummaryNoDeps'.+ -- This may have to change in the future.+ map extendModSummaryNoDeps $+#endif+ (catMaybes mss)+#endif+ pure (fingerprintToBS $ Util.fingerprintFingerprints $ map (maybe fingerprint0 msrFingerprint) msrs, processDependencyInformation rawDepInfo bm mg)+ -- This is factored out so it can be directly called from the GetModIface -- rule. Directly calling this rule means that on the initial load we can -- garbage collect all the intermediate typechecked modules rather than@@ -750,11 +775,11 @@ ghcSessionDepsDefinition fullModSummary ghcSessionDepsConfig env file newtype GhcSessionDepsConfig = GhcSessionDepsConfig- { checkForImportCycles :: Bool+ { fullModuleGraph :: Bool } instance Default GhcSessionDepsConfig where def = GhcSessionDepsConfig- { checkForImportCycles = True+ { fullModuleGraph = True } -- | Note [GhcSessionDeps]@@ -773,7 +798,7 @@ case mbdeps of Nothing -> return Nothing Just deps -> do- when checkForImportCycles $ void $ uses_ ReportImportCycles deps+ when fullModuleGraph $ void $ use_ ReportImportCycles file ms <- msrModSummary <$> if fullModSummary then use_ GetModSummary file else use_ GetModSummaryWithoutTimestamps file@@ -781,21 +806,40 @@ depSessions <- map hscEnv <$> uses_ (GhcSessionDeps_ fullModSummary) deps ifaces <- uses_ GetModIface deps let inLoadOrder = map (\HiFileResult{..} -> HomeModInfo hirModIface hirModDetails emptyHomeModInfoLinkable) ifaces+ mg <- do+ if fullModuleGraph+ then depModuleGraph <$> useNoFile_ GetModuleGraph+ else do+ let mgs = map hsc_mod_graph depSessions #if MIN_VERSION_ghc(9,3,0)- -- On GHC 9.4+, the module graph contains not only ModSummary's but each `ModuleNode` in the graph- -- also points to all the direct descendants of the current module. To get the keys for the descendants- -- we must get their `ModSummary`s- !final_deps <- do- dep_mss <- map msrModSummary <$> uses_ GetModSummaryWithoutTimestamps deps- -- Don't want to retain references to the entire ModSummary when just the key will do- return $!! map (NodeKey_Module . msKey) dep_mss- let moduleNode = (ms, final_deps)+ -- On GHC 9.4+, the module graph contains not only ModSummary's but each `ModuleNode` in the graph+ -- also points to all the direct descendants of the current module. To get the keys for the descendants+ -- we must get their `ModSummary`s+ !final_deps <- do+ dep_mss <- map msrModSummary <$> uses_ GetModSummaryWithoutTimestamps deps+ return $!! map (NodeKey_Module . msKey) dep_mss+ let module_graph_nodes =+ nubOrdOn mkNodeKey (ModuleNode final_deps ms : concatMap mgModSummaries' mgs) #else- let moduleNode = ms+ let module_graph_nodes =+#if MIN_VERSION_ghc(9,2,0)+ -- We don't do any instantiation for backpack at this point of time, so it is OK to use+ -- 'extendModSummaryNoDeps'.+ -- This may have to change in the future.+ map extendModSummaryNoDeps $ #endif- session' <- liftIO $ mergeEnvs hsc moduleNode inLoadOrder depSessions+ nubOrdOn ms_mod (ms : concatMap mgModSummaries mgs)+#endif+ liftIO $ evaluate $ liftRnf rwhnf module_graph_nodes+ return $ mkModuleGraph module_graph_nodes+ session' <- liftIO $ mergeEnvs hsc mg ms inLoadOrder depSessions - Just <$> liftIO (newHscEnvEqWithImportPaths (envImportPaths env) session' [])+ -- Here we avoid a call to to `newHscEnvEqWithImportPaths`, which creates a new+ -- ExportsMap when it is called. We only need to create the ExportsMap once per+ -- session, while `ghcSessionDepsDefinition` will be called for each file we need+ -- to compile. `updateHscEnvEq` will refresh the HscEnv (session') and also+ -- generate a new Unique.+ Just <$> liftIO (updateHscEnvEq env session') -- | Load a iface from disk, or generate it if there isn't one or it is out of date -- This rule also ensures that the `.hie` and `.o` (if needed) files are written out.@@ -855,7 +899,7 @@ -> do -- All good, the db has indexed the file when (coerce $ ideTesting se) $ liftIO $ mRunLspT (lspEnv se) $- LSP.sendNotification (SCustomMethod "ghcide/reference/ready") $+ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/reference/ready")) $ toJSON $ fromNormalizedFilePath f -- Not in db, must re-index _ -> do@@ -1192,8 +1236,16 @@ instance IsIdeGlobal CompiledLinkables data RulesConfig = RulesConfig- { -- | Disable import cycle checking for improved performance in large codebases- checkForImportCycles :: Bool+ { -- | Share the computation for the entire module graph+ -- We usually compute the full module graph for the project+ -- and share it for all files.+ -- However, in large projects it might not be desirable to wait+ -- for computing the entire module graph before starting to+ -- typecheck a particular file.+ -- Disabling this drastically decreases sharing and is likely to+ -- increase memory usage if you have multiple files open+ -- Disabling this also disables checking for import cycles+ fullModuleGraph :: Bool -- | Disable TH for improved performance in large codebases , enableTemplateHaskell :: Bool -- | Warning to show when TH is not supported by the current HLS binary@@ -1206,8 +1258,8 @@ displayTHWarning :: LspT c IO () displayTHWarning | not isWindows && not hostIsDynamic = do- LSP.sendNotification SWindowShowMessage $- ShowMessageParams MtInfo thWarningMessage+ LSP.sendNotification SMethod_WindowShowMessage $+ ShowMessageParams MessageType_Info thWarningMessage | otherwise = return () thWarningMessage :: T.Text@@ -1227,11 +1279,10 @@ getParsedModuleRule recorder getParsedModuleWithCommentsRule recorder getLocatedImportsRule recorder- getDependencyInformationRule recorder reportImportCyclesRule recorder typeCheckRule recorder getDocMapRule recorder- loadGhcSession recorder def{checkForImportCycles}+ loadGhcSession recorder def{fullModuleGraph} getModIfaceFromDiskRule recorder getModIfaceFromDiskAndIndexRule recorder getModIfaceRule recorder
src/Development/IDE/Core/Service.hs view
@@ -23,16 +23,16 @@ import Development.IDE.Core.FileExists (fileExistsRules) import Development.IDE.Core.OfInterest hiding (Log, LogShake) import Development.IDE.Graph-import Development.IDE.Types.Logger as Logger (Logger,+import Development.IDE.Types.Options (IdeOptions (..))+import Ide.Logger as Logger (Logger, Pretty (pretty), Priority (Debug), Recorder, WithPriority, cmapWithPrio)-import Development.IDE.Types.Options (IdeOptions (..)) import Ide.Plugin.Config+import qualified Language.LSP.Protocol.Types as LSP import qualified Language.LSP.Server as LSP-import qualified Language.LSP.Types as LSP import Control.Monad import qualified Development.IDE.Core.FileExists as FileExists
src/Development/IDE/Core/Shake.hs view
@@ -77,7 +77,7 @@ garbageCollectDirtyKeys, garbageCollectDirtyKeysOlderThan, Log(..),- VFSModified(..), getClientConfigAction+ VFSModified(..), getClientConfigAction, ) where import Control.Concurrent.Async@@ -152,8 +152,6 @@ import qualified Development.IDE.Types.Exports as ExportsMap import Development.IDE.Types.KnownTargets import Development.IDE.Types.Location-import Development.IDE.Types.Logger hiding (Priority)-import qualified Development.IDE.Types.Logger as Logger import Development.IDE.Types.Monitoring (Monitoring (..)) import Development.IDE.Types.Options import Development.IDE.Types.Shake@@ -161,16 +159,18 @@ import GHC.Fingerprint import GHC.Stack (HasCallStack) import HieDb.Types+import Ide.Logger hiding (Priority)+import qualified Ide.Logger as Logger import Ide.Plugin.Config import qualified Ide.PluginUtils as HLS import Ide.Types (IdePlugins (IdePlugins), PluginDescriptor (pluginId), PluginId) import Language.LSP.Diagnostics+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types+import qualified Language.LSP.Protocol.Types as LSP import qualified Language.LSP.Server as LSP-import Language.LSP.Types-import qualified Language.LSP.Types as LSP-import Language.LSP.Types.Capabilities import Language.LSP.VFS hiding (start) import qualified "list-t" ListT import OpenTelemetry.Eventlog@@ -303,7 +303,7 @@ type WithIndefiniteProgressFunc = forall a. T.Text -> LSP.ProgressCancellable -> IO a -> IO a -type GetStalePersistent = NormalizedFilePath -> IdeAction (Maybe (Dynamic,PositionDelta,TextDocumentVersion))+type GetStalePersistent = NormalizedFilePath -> IdeAction (Maybe (Dynamic,PositionDelta,Maybe Int32)) getShakeExtras :: Action ShakeExtras getShakeExtras = do@@ -344,7 +344,7 @@ -- This is called when we don't already have a result, or computing the rule failed. -- The result of this function will always be marked as 'stale', and a 'proper' rebuild of the rule will -- be queued if the rule hasn't run before.-addPersistentRule :: IdeRule k v => k -> (NormalizedFilePath -> IdeAction (Maybe (v,PositionDelta,TextDocumentVersion))) -> Rules ()+addPersistentRule :: IdeRule k v => k -> (NormalizedFilePath -> IdeAction (Maybe (v,PositionDelta,Maybe Int32))) -> Rules () addPersistentRule k getVal = do ShakeExtras{persistentKeys} <- getShakeExtrasRules void $ liftIO $ atomically $ modifyTVar' persistentKeys $ insertKeyMap (newKey k) (fmap (fmap (first3 toDyn)) . getVal)@@ -639,7 +639,6 @@ actionQueue <- newQueue let clientCapabilities = maybe def LSP.resClientCapabilities lspEnv- dirtyKeys <- newTVarIO mempty -- Take one VFS snapshot at the start vfsVar <- newTVarIO =<< vfsSnapshot lspEnv@@ -900,7 +899,7 @@ logDebug logger $ T.pack $ label <> " of " <> show n <> " keys (took " <> showDuration t <> ")" when (coerce ideTesting) $ liftIO $ mRunLspT lspEnv $- LSP.sendNotification (SCustomMethod "ghcide/GC")+ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/GC")) (toJSON $ mapMaybe (fmap showKey . fromKeyType) garbage) return garbage @@ -962,13 +961,23 @@ => k -> NormalizedFilePath -> Action (Maybe (v, PositionMapping)) useWithStale key file = runIdentity <$> usesWithStale key (Identity file) --- | Request a Rule result, it not available return the last computed result which may be stale.--- Errors out if none available.+-- |Request a Rule result, it not available return the last computed result+-- which may be stale.+--+-- Throws an `BadDependency` exception which is caught by the rule system if+-- none available.+--+-- WARNING: Not suitable for PluginHandlers. Use `useWithStaleE` instead. useWithStale_ :: IdeRule k v => k -> NormalizedFilePath -> Action (v, PositionMapping) useWithStale_ key file = runIdentity <$> usesWithStale_ key (Identity file) --- | Plural version of 'useWithStale_'+-- |Plural version of 'useWithStale_'+--+-- Throws an `BadDependency` exception which is caught by the rule system if+-- none available.+--+-- WARNING: Not suitable for PluginHandlers. usesWithStale_ :: (Traversable f, IdeRule k v) => k -> f NormalizedFilePath -> Action (f (v, PositionMapping)) usesWithStale_ key files = do res <- usesWithStale key files@@ -1042,12 +1051,24 @@ useNoFile :: IdeRule k v => k -> Action (Maybe v) useNoFile key = use key emptyFilePath +-- Requests a rule if available.+--+-- Throws an `BadDependency` exception which is caught by the rule system if+-- none available.+--+-- WARNING: Not suitable for PluginHandlers. Use `useE` instead. use_ :: IdeRule k v => k -> NormalizedFilePath -> Action v use_ key file = runIdentity <$> uses_ key (Identity file) useNoFile_ :: IdeRule k v => k -> Action v useNoFile_ key = use_ key emptyFilePath +-- |Plural version of `use_`+--+-- Throws an `BadDependency` exception which is caught by the rule system if+-- none available.+--+-- WARNING: Not suitable for PluginHandlers. Use `usesE` instead. uses_ :: (Traversable f, IdeRule k v) => k -> f NormalizedFilePath -> Action (f v) uses_ key files = do res <- uses key files@@ -1128,7 +1149,7 @@ defineEarlyCutoff' :: forall k v. IdeRule k v- => (TextDocumentVersion -> [FileDiagnostic] -> Action ()) -- ^ update diagnostics+ => (Maybe Int32 -> [FileDiagnostic] -> Action ()) -- ^ update diagnostics -- | compare current and previous for freshness -> (BS.ByteString -> BS.ByteString -> Bool) -> k@@ -1220,7 +1241,7 @@ updateFileDiagnostics :: MonadIO m => Recorder (WithPriority Log) -> NormalizedFilePath- -> TextDocumentVersion+ -> Maybe Int32 -> Key -> ShakeExtras -> [(ShowDiagnostic,Diagnostic)] -- ^ current results@@ -1254,15 +1275,15 @@ Just env -> LSP.runLspT env $ do liftIO $ tag "count" (show $ Prelude.length newDiags) liftIO $ tag "key" (show k)- LSP.sendNotification LSP.STextDocumentPublishDiagnostics $- LSP.PublishDiagnosticsParams (fromNormalizedUri uri) (fmap fromIntegral ver) (List newDiags)+ LSP.sendNotification SMethod_TextDocumentPublishDiagnostics $+ LSP.PublishDiagnosticsParams (fromNormalizedUri uri) (fmap fromIntegral ver) ( newDiags) return action where diagsFromRule :: Diagnostic -> Diagnostic diagsFromRule c@Diagnostic{_range} | coerce ideTesting = c {_relatedInformation =- Just $ List [+ Just $ [ DiagnosticRelatedInformation (Location (filePathToUri $ fromNormalizedFilePath fp)@@ -1297,7 +1318,7 @@ (forall a. String -> String -> a -> a) -> STMDiagnosticStore -> NormalizedUri ->- TextDocumentVersion ->+ Maybe Int32 -> DiagnosticsBySource -> STM [LSP.Diagnostic] updateSTMDiagnostics addTag store uri mv newDiagsBySource =@@ -1314,7 +1335,7 @@ setStageDiagnostics :: (forall a. String -> String -> a -> a) -> NormalizedUri- -> TextDocumentVersion -- ^ the time that the file these diagnostics originate from was last edited+ -> Maybe Int32 -- ^ the time that the file these diagnostics originate from was last edited -> T.Text -> [LSP.Diagnostic] -> STMDiagnosticStore@@ -1329,8 +1350,8 @@ getAllDiagnostics = fmap (concatMap (\(k,v) -> map (fromUri k,ShowDiag,) $ getDiagnosticsFromStore v)) . ListT.toList . STM.listT -updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> List TextDocumentContentChangeEvent -> STM ()-updatePositionMapping IdeState{shakeExtras = ShakeExtras{positionMapping}} VersionedTextDocumentIdentifier{..} (List changes) =+updatePositionMapping :: IdeState -> VersionedTextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> STM ()+updatePositionMapping IdeState{shakeExtras = ShakeExtras{positionMapping}} VersionedTextDocumentIdentifier{..} changes = STM.focus (Focus.alter f) uri positionMapping where uri = toNormalizedUri _uri@@ -1341,8 +1362,5 @@ -- used which is evident in long running sessions. EM.mapAccumRWithKey (\acc _k (delta, _) -> let new = addDelta delta acc in (new, (delta, acc))) zeroMapping- (EM.insert actual_version (shared_change, zeroMapping) mappingForUri)+ (EM.insert _version (shared_change, zeroMapping) mappingForUri) shared_change = mkDelta changes- actual_version = case _version of- Nothing -> error "Nothing version from server" -- This is a violation of the spec- Just v -> v
src/Development/IDE/Core/Tracing.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE PackageImports #-} {-# LANGUAGE PatternSynonyms #-} {-# HLINT ignore #-}+ module Development.IDE.Core.Tracing ( otTracedHandler , otTracedAction@@ -29,9 +30,9 @@ import Development.IDE.Types.Diagnostics (FileDiagnostic, showDiagnostics) import Development.IDE.Types.Location (Uri (..))-import Development.IDE.Types.Logger (Logger (Logger))+import Ide.Logger (Logger (Logger)) import Ide.Types (PluginId (..))-import Language.LSP.Types (NormalizedFilePath,+import Language.LSP.Protocol.Types (NormalizedFilePath, fromNormalizedFilePath) import OpenTelemetry.Eventlog (SpanInFlight (..), addEvent, beginSpan, endSpan, setTag,
src/Development/IDE/GHC/Compat/Core.hs view
@@ -39,6 +39,7 @@ lookupType, needWiredInHomeIface, loadWiredInHomeIface,+ readIface, loadSysInterface, importDecl, #if MIN_VERSION_ghc(8,8,0)
src/Development/IDE/GHC/Compat/Units.hs view
@@ -50,50 +50,54 @@ filterInplaceUnits, FinderCache, showSDocForUser',+ findImportedModule, ) where import Control.Monad-import qualified Data.List.NonEmpty as NE-import qualified Data.Map.Strict as Map+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as Map #if MIN_VERSION_ghc(9,3,0) import GHC.Unit.Home.ModInfo #endif #if MIN_VERSION_ghc(9,0,0) #if MIN_VERSION_ghc(9,2,0)-import qualified GHC.Data.ShortText as ST+import qualified GHC.Data.ShortText as ST #if !MIN_VERSION_ghc(9,3,0)-import GHC.Driver.Env (hsc_unit_dbs)+import GHC.Driver.Env (hsc_unit_dbs) #endif import GHC.Driver.Ppr import GHC.Unit.Env import GHC.Unit.External-import GHC.Unit.Finder+import GHC.Unit.Finder hiding+ (findImportedModule) #else import GHC.Driver.Types #endif import GHC.Data.FastString-import qualified GHC.Driver.Session as DynFlags+import qualified GHC.Driver.Session as DynFlags import GHC.Types.Unique.Set-import qualified GHC.Unit.Info as UnitInfo-import GHC.Unit.State (LookupResult, UnitInfo,- UnitState (unitInfoMap))-import qualified GHC.Unit.State as State-import GHC.Unit.Types hiding (moduleUnit, toUnitId)-import qualified GHC.Unit.Types as Unit+import qualified GHC.Unit.Info as UnitInfo+import GHC.Unit.State (LookupResult, UnitInfo,+ UnitState (unitInfoMap))+import qualified GHC.Unit.State as State+import GHC.Unit.Types hiding (moduleUnit,+ toUnitId)+import qualified GHC.Unit.Types as Unit import GHC.Utils.Outputable #else import qualified DynFlags import FastString-import GhcPlugins (SDoc, showSDocForUser)+import GhcPlugins (SDoc, showSDocForUser) import HscTypes-import Module hiding (moduleUnitId)+import Module hiding (moduleUnitId) import qualified Module-import Packages (InstalledPackageInfo (haddockInterfaces, packageName),- LookupResult, PackageConfig,- PackageConfigMap,- PackageState,- getPackageConfigMap,- lookupPackage')+import Packages (InstalledPackageInfo (haddockInterfaces, packageName),+ LookupResult,+ PackageConfig,+ PackageConfigMap,+ PackageState,+ getPackageConfigMap,+ lookupPackage') import qualified Packages #endif @@ -101,12 +105,23 @@ import Development.IDE.GHC.Compat.Env import Development.IDE.GHC.Compat.Outputable #if MIN_VERSION_ghc(9,0,0) && !MIN_VERSION_ghc(9,2,0)-import Data.Map (Map)+import Data.Map (Map) #endif import Data.Either import Data.Version import qualified GHC +#if MIN_VERSION_ghc(9,3,0)+import GHC.Types.PkgQual (PkgQual (NoPkgQual))+#endif+#if MIN_VERSION_ghc(9,1,0)+import qualified GHC.Unit.Finder as GHC+#elif MIN_VERSION_ghc(9,0,0)+import qualified GHC.Driver.Finder as GHC+#else+import qualified Finder as GHC+#endif+ #if MIN_VERSION_ghc(9,0,0) type PreloadUnitClosure = UniqSet UnitId #if MIN_VERSION_ghc(9,2,0)@@ -407,3 +422,14 @@ #else showSDocForUser' env = showSDocForUser (hsc_dflags env) #endif++findImportedModule :: HscEnv -> ModuleName -> IO (Maybe Module)+findImportedModule env mn = do+#if MIN_VERSION_ghc(9,3,0)+ res <- GHC.findImportedModule env mn NoPkgQual+#else+ res <- GHC.findImportedModule env mn Nothing+#endif+ case res of+ Found _ mod -> pure . pure $ mod+ _ -> pure Nothing
src/Development/IDE/GHC/Error.hs view
@@ -44,7 +44,7 @@ import Development.IDE.Types.Diagnostics as D import Development.IDE.Types.Location import GHC-import Language.LSP.Types (isSubrangeOf)+import Language.LSP.Protocol.Types (isSubrangeOf) diagFromText :: T.Text -> D.DiagnosticSeverity -> SrcSpan -> T.Text -> FileDiagnostic@@ -57,6 +57,8 @@ , _code = Nothing , _relatedInformation = Nothing , _tags = Nothing+ , _codeDescription = Nothing+ , _data_ = Nothing } -- | Produce a GHC-style error from a source span and a message.@@ -132,13 +134,13 @@ toDSeverity SevOutput = Nothing toDSeverity SevInteractive = Nothing toDSeverity SevDump = Nothing-toDSeverity SevInfo = Just DsInfo-toDSeverity SevFatal = Just DsError+toDSeverity SevInfo = Just DiagnosticSeverity_Information+toDSeverity SevFatal = Just DiagnosticSeverity_Error #else toDSeverity SevIgnore = Nothing #endif-toDSeverity SevWarning = Just DsWarning-toDSeverity SevError = Just DsError+toDSeverity SevWarning = Just DiagnosticSeverity_Warning+toDSeverity SevError = Just DiagnosticSeverity_Error -- | Produce a bag of GHC-style errors (@ErrorMessages@) from the given@@ -186,7 +188,7 @@ diagFromGhcException :: T.Text -> DynFlags -> GhcException -> [FileDiagnostic]-diagFromGhcException diagSource dflags exc = diagFromString diagSource DsError (noSpan "<Internal>") (showGHCE dflags exc)+diagFromGhcException diagSource dflags exc = diagFromString diagSource DiagnosticSeverity_Error (noSpan "<Internal>") (showGHCE dflags exc) showGHCE :: DynFlags -> GhcException -> String showGHCE dflags exc = case exc of
src/Development/IDE/GHC/Orphans.hs view
@@ -40,6 +40,7 @@ import Data.Text (unpack) #if MIN_VERSION_ghc(9,0,0) import GHC.ByteCode.Types+import GHC (ModuleGraph) #else import ByteCodeTypes #endif@@ -215,6 +216,9 @@ instance Hashable OccName where hashWithSalt s n = hashWithSalt s (getKey $ getUnique n) instance Show HomeModInfo where show = show . mi_module . hm_iface++instance Show ModuleGraph where show _ = "ModuleGraph {..}"+instance NFData ModuleGraph where rnf = rwhnf instance NFData HomeModInfo where rnf (HomeModInfo iface dets link) = rwhnf iface `seq` rnf dets `seq` rnf link
src/Development/IDE/GHC/Warnings.hs view
@@ -12,7 +12,7 @@ import Development.IDE.GHC.Compat import Development.IDE.GHC.Error import Development.IDE.Types.Diagnostics-import Language.LSP.Types (type (|?) (..))+import Language.LSP.Protocol.Types (type (|?) (..)) -- | Take a GHC monadic action (e.g. @typecheckModule pm@ for some
src/Development/IDE/Import/DependencyInformation.hs view
@@ -10,8 +10,9 @@ , TransitiveDependencies(..) , FilePathId(..) , NamedModuleDep(..)- , ShowableModuleName(..)- , PathIdMap+ , ShowableModule(..)+ , ShowableModuleEnv(..)+ , PathIdMap (..) , emptyPathIdMap , getPathId , lookupPathToId@@ -23,7 +24,7 @@ , transitiveDeps , transitiveReverseDependencies , immediateReverseDependencies-+ , lookupModuleFile , BootIdMap , insertBootId ) where@@ -53,6 +54,7 @@ import Development.IDE.Types.Location import GHC+import Development.IDE.GHC.Compat -- | The imports for a given module. newtype ModuleImports = ModuleImports@@ -103,8 +105,8 @@ insertImport :: FilePathId -> Either ModuleParseError ModuleImports -> RawDependencyInformation -> RawDependencyInformation insertImport (FilePathId k) v rawDepInfo = rawDepInfo { rawImports = IntMap.insert k v (rawImports rawDepInfo) } -pathToId :: PathIdMap -> NormalizedFilePath -> FilePathId-pathToId PathIdMap{pathToIdMap} path = pathToIdMap HMS.! path+pathToId :: PathIdMap -> NormalizedFilePath -> Maybe FilePathId+pathToId PathIdMap{pathToIdMap} path = pathToIdMap HMS.!? path lookupPathToId :: PathIdMap -> NormalizedFilePath -> Maybe FilePathId lookupPathToId PathIdMap{pathToIdMap} path = HMS.lookup path pathToIdMap@@ -128,15 +130,14 @@ -- corresponding hs file. It is used when topologically sorting as we -- need to add edges between .hs-boot and .hs so that the .hs files -- appear later in the sort.- , rawBootMap :: !BootIdMap- , rawModuleNameMap :: !(FilePathIdMap ShowableModuleName)+ , rawModuleMap :: !(FilePathIdMap ShowableModule) } deriving Show data DependencyInformation = DependencyInformation { depErrorNodes :: !(FilePathIdMap (NonEmpty NodeError)) -- ^ Nodes that cannot be processed correctly.- , depModuleNames :: !(FilePathIdMap ShowableModuleName)+ , depModules :: !(FilePathIdMap ShowableModule) , depModuleDeps :: !(FilePathIdMap FilePathIdSet) -- ^ For a non-error node, this contains the set of module immediate dependencies -- in the same package.@@ -146,14 +147,25 @@ -- ^ Map from FilePath to FilePathId , depBootMap :: !BootIdMap -- ^ Map from hs-boot file to the corresponding hs file+ , depModuleFiles :: !(ShowableModuleEnv FilePathId)+ -- ^ Map from Module to the corresponding non-boot hs file+ , depModuleGraph :: !ModuleGraph } deriving (Show, Generic) -newtype ShowableModuleName =- ShowableModuleName {showableModuleName :: ModuleName}+newtype ShowableModule =+ ShowableModule {showableModule :: Module} deriving NFData -instance Show ShowableModuleName where show = moduleNameString . showableModuleName+newtype ShowableModuleEnv a =+ ShowableModuleEnv {showableModuleEnv :: ModuleEnv a} +instance Show a => Show (ShowableModuleEnv a) where+ show (ShowableModuleEnv x) = show (moduleEnvToList x)+instance NFData a => NFData (ShowableModuleEnv a) where+ rnf = rwhnf++instance Show ShowableModule where show = moduleNameString . moduleName . showableModule+ reachableModules :: DependencyInformation -> [NormalizedFilePath] reachableModules DependencyInformation{..} = map (idToPath depPathIdMap . FilePathId) $ IntMap.keys depErrorNodes <> IntMap.keys depModuleDeps@@ -215,15 +227,17 @@ SuccessNode _ <> ErrorNode errs = ErrorNode errs SuccessNode a <> SuccessNode _ = SuccessNode a -processDependencyInformation :: RawDependencyInformation -> DependencyInformation-processDependencyInformation RawDependencyInformation{..} =+processDependencyInformation :: RawDependencyInformation -> BootIdMap -> ModuleGraph -> DependencyInformation+processDependencyInformation RawDependencyInformation{..} rawBootMap mg = DependencyInformation { depErrorNodes = IntMap.fromList errorNodes , depModuleDeps = moduleDeps , depReverseModuleDeps = reverseModuleDeps- , depModuleNames = rawModuleNameMap+ , depModules = rawModuleMap , depPathIdMap = rawPathIdMap , depBootMap = rawBootMap+ , depModuleFiles = ShowableModuleEnv reverseModuleMap+ , depModuleGraph = mg } where resultGraph = buildResultGraph rawImports (errorNodes, successNodes) = partitionNodeResults $ IntMap.toList resultGraph@@ -240,6 +254,7 @@ foldr (\(p, cs) res -> let new = IntMap.fromList (map (, IntSet.singleton (coerce p)) (coerce cs)) in IntMap.unionWith IntSet.union new res ) IntMap.empty successEdges+ reverseModuleMap = mkModuleEnv $ map (\(i,sm) -> (showableModule sm, FilePathId i)) $ IntMap.toList rawModuleMap -- | Given a dependency graph, buildResultGraph detects and propagates errors in that graph as follows:@@ -328,7 +343,7 @@ -- | returns all transitive dependencies in topological order. transitiveDeps :: DependencyInformation -> NormalizedFilePath -> Maybe TransitiveDependencies transitiveDeps DependencyInformation{..} file = do- let !fileId = pathToId depPathIdMap file+ !fileId <- pathToId depPathIdMap file reachableVs <- -- Delete the starting node IntSet.delete (getFilePathId fileId) .@@ -350,6 +365,10 @@ boot_edge f = [getFilePathId f' | Just f' <- [IntMap.lookup f depBootMap]] vs = topSort g++lookupModuleFile :: Module -> DependencyInformation -> Maybe NormalizedFilePath+lookupModuleFile mod DependencyInformation{..}+ = idToPath depPathIdMap <$> lookupModuleEnv (showableModuleEnv depModuleFiles) mod newtype TransitiveDependencies = TransitiveDependencies { transitiveModuleDeps :: [NormalizedFilePath]
src/Development/IDE/Import/FindImports.hs view
@@ -29,6 +29,7 @@ import System.FilePath #if MIN_VERSION_ghc(9,3,0) import GHC.Types.PkgQual+import GHC.Unit.State #endif data Import@@ -135,25 +136,45 @@ #else Nothing -> do #endif++ mbFile <- locateModuleFile ((homeUnitId_ dflags, importPaths dflags) : other_imports) exts targetFor isSource $ unLoc modName+ case mbFile of+ Nothing -> lookupInPackageDB env+ Just (uid, file) -> toModLocation uid file+ where+ dflags = hsc_dflags env+ import_paths = mapMaybe (mkImportDirs env) comp_info+ other_imports =+#if MIN_VERSION_ghc(9,4,0)+ -- On 9.4+ instead of bringing all the units into scope, only bring into scope the units+ -- this one depends on+ -- This way if you have multiple units with the same module names, we won't get confused+ -- For example if unit a imports module M from unit B, when there is also a module M in unit C,+ -- and unit a only depends on unit b, without this logic there is the potential to get confused+ -- about which module unit a imports.+ -- Without multi-component support it is hard to recontruct the dependency environment so+ -- unit a will have both unit b and unit c in scope.+ map (\uid -> (uid, importPaths (homeUnitEnv_dflags (ue_findHomeUnitEnv uid ue)))) hpt_deps+ ue = hsc_unit_env env+ units = homeUnitEnv_units $ ue_findHomeUnitEnv (homeUnitId_ dflags) ue+ hpt_deps :: [UnitId]+ hpt_deps = homeUnitDepends units+#else+ import_paths'+#endif+ -- first try to find the module as a file. If we can't find it try to find it in the package -- database. -- Here the importPaths for the current modules are added to the front of the import paths from the other components. -- This is particularly important for Paths_* modules which get generated for every component but unless you use it in -- each component will end up being found in the wrong place and cause a multi-cradle match failure.- let import_paths' =+ import_paths' = #if MIN_VERSION_ghc(9,3,0) import_paths #else map snd import_paths #endif - mbFile <- locateModuleFile ((homeUnitId_ dflags, importPaths dflags) : import_paths') exts targetFor isSource $ unLoc modName- case mbFile of- Nothing -> lookupInPackageDB env- Just (uid, file) -> toModLocation uid file- where- dflags = hsc_dflags env- import_paths = mapMaybe (mkImportDirs env) comp_info toModLocation uid file = liftIO $ do loc <- mkHomeModLocation dflags (unLoc modName) (fromNormalizedFilePath file) #if MIN_VERSION_ghc(9,0,0)@@ -180,7 +201,7 @@ mkError' $ ppr' $ cannotFindModule env modName0 $ lookupToFindResult reason where dfs = hsc_dflags env- mkError' = diagFromString "not found" DsError (Compat.getLoc modName)+ mkError' = diagFromString "not found" DiagnosticSeverity_Error (Compat.getLoc modName) modName0 = unLoc modName ppr' = showSDoc dfs -- We convert the lookup result to a find result to reuse GHC's cannotFindModule pretty printer.
src/Development/IDE/LSP/HoverDefinition.hs view
@@ -15,45 +15,47 @@ , wsSymbols ) where +import Control.Monad.Except (ExceptT) import Control.Monad.IO.Class+import Data.Maybe (fromMaybe) import Development.IDE.Core.Actions import Development.IDE.Core.Rules import Development.IDE.Core.Shake import Development.IDE.Types.Location-import Development.IDE.Types.Logger+import Ide.Logger+import Ide.Plugin.Error+import Ide.Types+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types import qualified Language.LSP.Server as LSP-import Language.LSP.Types import qualified Data.Text as T -gotoDefinition :: IdeState -> TextDocumentPositionParams -> LSP.LspM c (Either ResponseError (ResponseResult TextDocumentDefinition))-hover :: IdeState -> TextDocumentPositionParams -> LSP.LspM c (Either ResponseError (Maybe Hover))-gotoTypeDefinition :: IdeState -> TextDocumentPositionParams -> LSP.LspM c (Either ResponseError (ResponseResult TextDocumentTypeDefinition))-documentHighlight :: IdeState -> TextDocumentPositionParams -> LSP.LspM c (Either ResponseError (List DocumentHighlight))-gotoDefinition = request "Definition" getDefinition (InR $ InL $ List []) (InR . InL . List)-gotoTypeDefinition = request "TypeDefinition" getTypeDefinition (InR $ InL $ List []) (InR . InL . List)-hover = request "Hover" getAtPoint Nothing foundHover-documentHighlight = request "DocumentHighlight" highlightAtPoint (List []) List+gotoDefinition :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) (MessageResult Method_TextDocumentDefinition)+hover :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) (Hover |? Null)+gotoTypeDefinition :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) (MessageResult Method_TextDocumentTypeDefinition)+documentHighlight :: IdeState -> TextDocumentPositionParams -> ExceptT PluginError (LSP.LspM c) ([DocumentHighlight] |? Null)+gotoDefinition = request "Definition" getDefinition (InR $ InR Null) (InL . Definition. InR)+gotoTypeDefinition = request "TypeDefinition" getTypeDefinition (InR $ InR Null) (InL . Definition. InR)+hover = request "Hover" getAtPoint (InR Null) foundHover+documentHighlight = request "DocumentHighlight" highlightAtPoint (InR Null) InL -references :: IdeState -> ReferenceParams -> LSP.LspM c (Either ResponseError (List Location))-references ide (ReferenceParams (TextDocumentIdentifier uri) pos _ _ _) = liftIO $- case uriToFilePath' uri of- Just path -> do- let filePath = toNormalizedFilePath' path- logDebug (ideLogger ide) $+references :: PluginMethodHandler IdeState 'Method_TextDocumentReferences+references ide _ (ReferenceParams (TextDocumentIdentifier uri) pos _ _ _) = do+ nfp <- getNormalizedFilePathE uri+ liftIO $ logDebug (ideLogger ide) $ "References request at position " <> T.pack (showPosition pos) <>- " in file: " <> T.pack path- Right . List <$> (runAction "references" ide $ refsAtPoint filePath pos)- Nothing -> pure $ Left $ ResponseError InvalidParams ("Invalid URI " <> T.pack (show uri)) Nothing+ " in file: " <> T.pack (show nfp)+ InL <$> (liftIO $ runAction "references" ide $ refsAtPoint nfp pos) -wsSymbols :: IdeState -> WorkspaceSymbolParams -> LSP.LspM c (Either ResponseError (List SymbolInformation))-wsSymbols ide (WorkspaceSymbolParams _ _ query) = liftIO $ do+wsSymbols :: PluginMethodHandler IdeState 'Method_WorkspaceSymbol+wsSymbols ide _ (WorkspaceSymbolParams _ _ query) = liftIO $ do logDebug (ideLogger ide) $ "Workspace symbols request: " <> query- runIdeAction "WorkspaceSymbols" (shakeExtras ide) $ Right . maybe (List []) List <$> workspaceSymbols query+ runIdeAction "WorkspaceSymbols" (shakeExtras ide) $ InL . fromMaybe [] <$> workspaceSymbols query -foundHover :: (Maybe Range, [T.Text]) -> Maybe Hover+foundHover :: (Maybe Range, [T.Text]) -> Hover |? Null foundHover (mbRange, contents) =- Just $ Hover (HoverContents $ MarkupContent MkMarkdown $ T.intercalate sectionSeparator contents) mbRange+ InL $ Hover (InL $ MarkupContent MarkupKind_Markdown $ T.intercalate sectionSeparator contents) mbRange -- | Respond to and log a hover or go-to-definition request request@@ -63,12 +65,12 @@ -> (a -> b) -> IdeState -> TextDocumentPositionParams- -> LSP.LspM c (Either ResponseError b)+ -> ExceptT PluginError (LSP.LspM c) b request label getResults notFound found ide (TextDocumentPositionParams (TextDocumentIdentifier uri) pos) = liftIO $ do mbResult <- case uriToFilePath' uri of Just path -> logAndRunRequest label getResults ide pos path Nothing -> pure Nothing- pure $ Right $ maybe notFound found mbResult+ pure $ maybe notFound found mbResult logAndRunRequest :: T.Text -> (NormalizedFilePath -> Position -> IdeAction b) -> IdeState -> Position -> String -> IO b logAndRunRequest label getResults ide pos path = do
src/Development/IDE/LSP/LanguageServer.hs view
@@ -1,12 +1,12 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE StarIsType #-}- -- WARNING: A copy of DA.Daml.LanguageServer, try to keep them in sync -- This version removes the daml: handling module Development.IDE.LSP.LanguageServer@@ -26,8 +26,9 @@ import Development.IDE.LSP.Server import Development.IDE.Session (runWithDb) import Ide.Types (traceWithSpan)+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types import qualified Language.LSP.Server as LSP-import Language.LSP.Types import System.IO import UnliftIO.Async import UnliftIO.Concurrent@@ -40,14 +41,13 @@ import Development.IDE.Core.Shake hiding (Log, Priority) import Development.IDE.Core.Tracing import qualified Development.IDE.Session as Session-import Development.IDE.Types.Logger-import qualified Development.IDE.Types.Logger as Logger import Development.IDE.Types.Shake (WithHieDb)+import Ide.Logger+import qualified Ide.Logger as Logger import Language.LSP.Server (LanguageContextEnv, LspServerLog, type (<~>)) import System.IO.Unsafe (unsafeInterleaveIO)- data Log = LogRegisteringIdeConfig !IdeConfiguration | LogReactorThreadException !SomeException@@ -92,7 +92,7 @@ -> config -> (config -> Value -> Either T.Text config) -> (MVar ()- -> IO (LSP.LanguageContextEnv config -> RequestMessage Initialize -> IO (Either ResponseError (LSP.LanguageContextEnv config, a)),+ -> IO (LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either ResponseError (LSP.LanguageContextEnv config, a)), LSP.Handlers (m config), (LanguageContextEnv config, a) -> m config <~> IO)) -> IO ()@@ -107,7 +107,7 @@ { LSP.onConfigurationChange = onConfigurationChange , LSP.defaultConfig = defaultConfig , LSP.doInitialize = doInitialize- , LSP.staticHandlers = staticHandlers+ , LSP.staticHandlers = (const staticHandlers) , LSP.interpretHandler = interpretHandler , LSP.options = modifyOptions options }@@ -133,7 +133,7 @@ -> LSP.Handlers (ServerM config) -> (LSP.LanguageContextEnv config -> Maybe FilePath -> WithHieDb -> IndexQueue -> IO IdeState) -> MVar ()- -> IO (LSP.LanguageContextEnv config -> RequestMessage Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState)),+ -> IO (LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState)), LSP.Handlers (ServerM config), (LanguageContextEnv config, IdeState) -> ServerM config <~> IO) setupLSP recorder getHieDbLoc userHandlers getIdeState clientMsgVar = do@@ -195,8 +195,8 @@ -> (SomeLspId -> IO ()) -> (SomeLspId -> IO ()) -> Chan ReactorMessage- -> LSP.LanguageContextEnv config -> RequestMessage Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState))-handleInit recorder getHieDbLoc getIdeState lifetime exitClientMsg clearReqId waitForCancel clientMsgChan env (RequestMessage _ _ m params) = otTracedHandler "Initialize" (show m) $ \sp -> do+ -> LSP.LanguageContextEnv config -> TRequestMessage Method_Initialize -> IO (Either err (LSP.LanguageContextEnv config, IdeState))+handleInit recorder getHieDbLoc getIdeState lifetime exitClientMsg clearReqId waitForCancel clientMsgChan env (TRequestMessage _ _ m params) = otTracedHandler "Initialize" (show m) $ \sp -> do traceWithSpan sp params let root = LSP.resRootPath env dir <- maybe getCurrentDirectory return root@@ -233,11 +233,11 @@ case cancelOrRes of Left () -> do log Debug $ LogCancelledRequest _id- k $ ResponseError RequestCancelled "" Nothing+ k $ ResponseError (InL LSPErrorCodes_RequestCancelled) "" Nothing Right res -> pure res ) $ \(e :: SomeException) -> do exceptionInHandler e- k $ ResponseError InternalError (T.pack $ show e) Nothing+ k $ ResponseError (InR ErrorCodes_InternalError) (T.pack $ show e) Nothing _ <- flip forkFinally handleServerException $ do untilMVar lifetime $ runWithDb (cmapWithPrio LogSession recorder) dbLoc $ \withHieDb hieChan -> do putMVar dbMVar (WithHieDbShield withHieDb,hieChan)@@ -263,27 +263,30 @@ waitAnyCancel =<< traverse async [ io , readMVar mvar ] cancelHandler :: (SomeLspId -> IO ()) -> LSP.Handlers (ServerM c)-cancelHandler cancelRequest = LSP.notificationHandler SCancelRequest $ \NotificationMessage{_params=CancelParams{_id}} ->- liftIO $ cancelRequest (SomeLspId _id)+cancelHandler cancelRequest = LSP.notificationHandler SMethod_CancelRequest $ \TNotificationMessage{_params=CancelParams{_id}} ->+ liftIO $ cancelRequest (SomeLspId (toLspId _id))+ where toLspId :: (Int32 |? T.Text) -> LspId a+ toLspId (InL x) = IdInt x+ toLspId (InR y) = IdString y shutdownHandler :: IO () -> LSP.Handlers (ServerM c)-shutdownHandler stopReactor = LSP.requestHandler SShutdown $ \_ resp -> do+shutdownHandler stopReactor = LSP.requestHandler SMethod_Shutdown $ \_ resp -> do (_, ide) <- ask liftIO $ logDebug (ideLogger ide) "Received shutdown message" -- stop the reactor to free up the hiedb connection liftIO stopReactor -- flush out the Shake session to record a Shake profile if applicable liftIO $ shakeShut ide- resp $ Right Empty+ resp $ Right Null exitHandler :: IO () -> LSP.Handlers (ServerM c)-exitHandler exit = LSP.notificationHandler SExit $ const $ liftIO exit+exitHandler exit = LSP.notificationHandler SMethod_Exit $ const $ liftIO exit modifyOptions :: LSP.Options -> LSP.Options-modifyOptions x = x{ LSP.textDocumentSync = Just $ tweakTDS origTDS+modifyOptions x = x{ LSP.optTextDocumentSync = Just $ tweakTDS origTDS } where- tweakTDS tds = tds{_openClose=Just True, _change=Just TdSyncIncremental, _save=Just $ InR $ SaveOptions Nothing}- origTDS = fromMaybe tdsDefault $ LSP.textDocumentSync x+ tweakTDS tds = tds{_openClose=Just True, _change=Just TextDocumentSyncKind_Incremental, _save=Just $ InR $ SaveOptions Nothing}+ origTDS = fromMaybe tdsDefault $ LSP.optTextDocumentSync x tdsDefault = TextDocumentSyncOptions Nothing Nothing Nothing Nothing Nothing
src/Development/IDE/LSP/Notifications.hs view
@@ -13,8 +13,9 @@ , ghcideNotificationsPluginPriority ) where -import Language.LSP.Types-import qualified Language.LSP.Types as LSP+import qualified Language.LSP.Protocol.Message as LSP+import Language.LSP.Protocol.Types+import qualified Language.LSP.Protocol.Types as LSP import Control.Concurrent.STM.Stats (atomically) import Control.Monad.Extra@@ -36,8 +37,8 @@ import Development.IDE.Core.Shake hiding (Log, Priority) import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Types.Location-import Development.IDE.Types.Logger import Development.IDE.Types.Shake (toKey)+import Ide.Logger import Ide.Types import Numeric.Natural @@ -56,9 +57,9 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = (defaultPluginDescriptor plId) { pluginNotificationHandlers = mconcat- [ mkPluginNotificationHandler LSP.STextDocumentDidOpen $+ [ mkPluginNotificationHandler LSP.SMethod_TextDocumentDidOpen $ \ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri,_version}) -> liftIO $ do- atomically $ updatePositionMapping ide (VersionedTextDocumentIdentifier _uri (Just _version)) (List [])+ atomically $ updatePositionMapping ide (VersionedTextDocumentIdentifier _uri _version) [] whenUriFile _uri $ \file -> do -- We don't know if the file actually exists, or if the contents match those on disk -- For example, vscode restores previously unsaved contents on open@@ -66,7 +67,7 @@ setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide False file logDebug (ideLogger ide) $ "Opened text document: " <> getUri _uri - , mkPluginNotificationHandler LSP.STextDocumentDidChange $+ , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidChange $ \ide vfs _ (DidChangeTextDocumentParams identifier@VersionedTextDocumentIdentifier{_uri} changes) -> liftIO $ do atomically $ updatePositionMapping ide identifier changes whenUriFile _uri $ \file -> do@@ -74,14 +75,14 @@ setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide False file logDebug (ideLogger ide) $ "Modified text document: " <> getUri _uri - , mkPluginNotificationHandler LSP.STextDocumentDidSave $+ , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidSave $ \ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do whenUriFile _uri $ \file -> do addFileOfInterest ide file OnDisk setFileModified (cmapWithPrio LogFileStore recorder) (VFSModified vfs) ide True file logDebug (ideLogger ide) $ "Saved text document: " <> getUri _uri - , mkPluginNotificationHandler LSP.STextDocumentDidClose $+ , mkPluginNotificationHandler LSP.SMethod_TextDocumentDidClose $ \ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do whenUriFile _uri $ \file -> do deleteFileOfInterest ide file@@ -90,8 +91,8 @@ setSomethingModified (VFSModified vfs) ide [] $ Text.unpack msg logDebug (ideLogger ide) msg - , mkPluginNotificationHandler LSP.SWorkspaceDidChangeWatchedFiles $- \ide vfs _ (DidChangeWatchedFilesParams (List fileEvents)) -> liftIO $ do+ , mkPluginNotificationHandler LSP.SMethod_WorkspaceDidChangeWatchedFiles $+ \ide vfs _ (DidChangeWatchedFilesParams fileEvents) -> liftIO $ do -- See Note [File existence cache and LSP file watchers] which explains why we get these notifications and -- what we do with them -- filter out files of interest, since we already know all about those@@ -110,7 +111,7 @@ resetFileStore ide fileEvents' setSomethingModified (VFSModified vfs) ide [] msg - , mkPluginNotificationHandler LSP.SWorkspaceDidChangeWorkspaceFolders $+ , mkPluginNotificationHandler LSP.SMethod_WorkspaceDidChangeWorkspaceFolders $ \ide _ _ (DidChangeWorkspaceFoldersParams events) -> liftIO $ do let add = S.union substract = flip S.difference@@ -118,14 +119,14 @@ $ add (foldMap (S.singleton . parseWorkspaceFolder) (_added events)) . substract (foldMap (S.singleton . parseWorkspaceFolder) (_removed events)) - , mkPluginNotificationHandler LSP.SWorkspaceDidChangeConfiguration $+ , mkPluginNotificationHandler LSP.SMethod_WorkspaceDidChangeConfiguration $ \ide vfs _ (DidChangeConfigurationParams cfg) -> liftIO $ do let msg = Text.pack $ show cfg logDebug (ideLogger ide) $ "Configuration changed: " <> msg modifyClientSettings ide (const $ Just cfg) setSomethingModified (VFSModified vfs) ide [toKey GetClientSettings emptyFilePath] "config change" - , mkPluginNotificationHandler LSP.SInitialized $ \ide _ _ _ -> do+ , mkPluginNotificationHandler LSP.SMethod_Initialized $ \ide _ _ _ -> do --------- Initialize Shake session -------------------------------------------------------------------- liftIO $ shakeSessionInit (cmapWithPrio LogShake recorder) ide
src/Development/IDE/LSP/Outline.hs view
@@ -22,26 +22,25 @@ realSrcSpanToRange) import Development.IDE.Types.Location import Development.IDE.GHC.Util (printOutputable)-import Language.LSP.Server (LspM)-import Language.LSP.Types (DocumentSymbol (..),+import Ide.Types+import Language.LSP.Protocol.Types (DocumentSymbol (..), DocumentSymbolParams (DocumentSymbolParams, _textDocument),- List (..), ResponseError,- SymbolInformation,- SymbolKind (SkConstructor, SkField, SkFile, SkFunction, SkInterface, SkMethod, SkModule, SkObject, SkStruct, SkTypeParameter, SkUnknown),+ SymbolKind (..), TextDocumentIdentifier (TextDocumentIdentifier),- type (|?) (InL), uriToFilePath)+ type (|?) (InL, InR), uriToFilePath)+import Language.LSP.Protocol.Message #if MIN_VERSION_ghc(9,2,0) import Data.List.NonEmpty (nonEmpty) #endif moduleOutline- :: IdeState -> DocumentSymbolParams -> LspM c (Either ResponseError (List DocumentSymbol |? List SymbolInformation))-moduleOutline ideState DocumentSymbolParams{ _textDocument = TextDocumentIdentifier uri }+ :: PluginMethodHandler IdeState 'Method_TextDocumentDocumentSymbol+moduleOutline ideState _ DocumentSymbolParams{ _textDocument = TextDocumentIdentifier uri } = liftIO $ case uriToFilePath uri of Just (toNormalizedFilePath' -> fp) -> do mb_decls <- fmap fst <$> runAction "Outline" ideState (useWithStale GetParsedModule fp)- pure $ Right $ case mb_decls of- Nothing -> InL (List [])+ pure $ case mb_decls of+ Nothing -> InL [] Just ParsedModule { pm_parsed_source = L _ltop HsModule { hsmodName, hsmodDecls, hsmodImports } } -> let declSymbols = mapMaybe documentSymbolForDecl hsmodDecls@@ -49,7 +48,7 @@ (L (locA -> (RealSrcSpan l _)) m) -> Just $ (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable m- , _kind = SkFile+ , _kind = SymbolKind_File , _range = Range (Position 0 0) (Position maxBound 0) -- _ltop is 0 0 0 0 } _ -> Nothing@@ -59,14 +58,14 @@ allSymbols = case moduleSymbol of Nothing -> importSymbols <> declSymbols Just x ->- [ x { _children = Just (List (importSymbols <> declSymbols))+ [ x { _children = Just (importSymbols <> declSymbols) } ] in- InL (List allSymbols)+ InR (InL allSymbols) - Nothing -> pure $ Right $ InL (List [])+ Nothing -> pure $ InL [] documentSymbolForDecl :: LHsDecl GhcPs -> Maybe DocumentSymbol documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (TyClD _ FamDecl { tcdFam = FamilyDecl { fdLName = L _ n, fdInfo, fdTyVars } }))@@ -77,7 +76,7 @@ t -> " " <> t ) , _detail = Just $ printOutputable fdInfo- , _kind = SkFunction+ , _kind = SymbolKind_Function } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (TyClD _ ClassDecl { tcdLName = L _ name, tcdSigs, tcdTyVars })) = Just (defDocumentSymbol l :: DocumentSymbol)@@ -86,13 +85,13 @@ "" -> "" t -> " " <> t )- , _kind = SkInterface+ , _kind = SymbolKind_Interface , _detail = Just "class" , _children =- Just $ List+ Just $ [ (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable n- , _kind = SkMethod+ , _kind = SymbolKind_Method , _selectionRange = realSrcSpanToRange l' } | L (locA -> (RealSrcSpan l _)) (ClassOpSig _ False names _) <- tcdSigs@@ -102,15 +101,15 @@ documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (TyClD _ DataDecl { tcdLName = L _ name, tcdDataDefn = HsDataDefn { dd_cons } })) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable name- , _kind = SkStruct+ , _kind = SymbolKind_Struct , _children =- Just $ List+ Just $ [ (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable n- , _kind = SkConstructor+ , _kind = SymbolKind_Constructor , _selectionRange = realSrcSpanToRange l' #if MIN_VERSION_ghc(9,2,0)- , _children = List . toList <$> nonEmpty childs+ , _children = toList <$> nonEmpty childs } | con <- extract_cons dd_cons , let (cs, flds) = hsConDeclsBinders con@@ -133,7 +132,7 @@ #else { _name = printOutputable (unLoc (rdrNameFieldOcc n)) #endif- , _kind = SkField+ , _kind = SymbolKind_Field } cvtFld _ = Nothing #else@@ -145,10 +144,10 @@ } where -- | Extract the record fields of a constructor- conArgRecordFields (RecCon (L _ lcdfs)) = Just $ List+ conArgRecordFields (RecCon (L _ lcdfs)) = Just [ (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable n- , _kind = SkField+ , _kind = SymbolKind_Field } | L _ cdf <- lcdfs , L (locA -> (RealSrcSpan l _)) n <- rdrNameFieldOcc . unLoc <$> cd_fld_names cdf@@ -157,12 +156,12 @@ #endif documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (TyClD _ SynDecl { tcdLName = L (locA -> (RealSrcSpan l' _)) n })) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable n- , _kind = SkTypeParameter+ , _kind = SymbolKind_TypeParameter , _selectionRange = realSrcSpanToRange l' } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (InstD _ ClsInstD { cid_inst = ClsInstDecl { cid_poly_ty } })) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable cid_poly_ty- , _kind = SkInterface+ , _kind = SymbolKind_Interface } #if MIN_VERSION_ghc(9,2,0) documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (InstD _ DataFamInstD { dfid_inst = DataFamInstDecl FamEqn { feqn_tycon, feqn_pats } }))@@ -177,7 +176,7 @@ printOutputable (unLoc feqn_tycon) <> " " <> T.unwords (map printOutputable feqn_pats) #endif- , _kind = SkInterface+ , _kind = SymbolKind_Interface } #if MIN_VERSION_ghc(9,2,0) documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (InstD _ TyFamInstD { tfid_inst = TyFamInstDecl _ FamEqn { feqn_tycon, feqn_pats } }))@@ -192,23 +191,23 @@ printOutputable (unLoc feqn_tycon) <> " " <> T.unwords (map printOutputable feqn_pats) #endif- , _kind = SkInterface+ , _kind = SymbolKind_Interface } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (DerivD _ DerivDecl { deriv_type })) = gfindtype deriv_type <&> \(L (_ :: SrcSpan) name) -> (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable @(HsType GhcPs) name- , _kind = SkInterface+ , _kind = SymbolKind_Interface } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (ValD _ FunBind{fun_id = L _ name})) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable name- , _kind = SkFunction+ , _kind = SymbolKind_Function } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (ValD _ PatBind{pat_lhs})) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = printOutputable pat_lhs- , _kind = SkFunction+ , _kind = SymbolKind_Function } documentSymbolForDecl (L (locA -> (RealSrcSpan l _)) (ForD _ x)) = Just@@ -217,7 +216,7 @@ ForeignImport{} -> name ForeignExport{} -> name XForeignDecl{} -> "?"- , _kind = SkObject+ , _kind = SymbolKind_Object , _detail = case x of ForeignImport{} -> Just "import" ForeignExport{} -> Just "export"@@ -240,15 +239,15 @@ in Just (defDocumentSymbol (rangeToRealSrcSpan "" importRange)) { _name = "imports"- , _kind = SkModule- , _children = Just (List importSymbols)+ , _kind = SymbolKind_Module+ , _children = Just importSymbols } documentSymbolForImport :: LImportDecl GhcPs -> Maybe DocumentSymbol documentSymbolForImport (L (locA -> (RealSrcSpan l _)) ImportDecl { ideclName, ideclQualified }) = Just (defDocumentSymbol l :: DocumentSymbol) { _name = "import " <> printOutputable ideclName- , _kind = SkModule+ , _kind = SymbolKind_Module , _detail = case ideclQualified of { NotQualified -> Nothing; _ -> Just "qualified" } } documentSymbolForImport _ = Nothing@@ -258,7 +257,9 @@ _detail = Nothing _deprecated = Nothing _name = ""- _kind = SkUnknown 0+ -- This used to be SkUnknown 0, which is invalid, as SymbolKinds start at 1,+ -- therefore, I am replacing it with SymbolKind_File, which is the type for 1+ _kind = SymbolKind_File _range = realSrcSpanToRange l _selectionRange = realSrcSpanToRange l _children = Nothing
src/Development/IDE/LSP/Server.hs view
@@ -1,12 +1,8 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE UndecidableInstances #-} -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE RankNTypes #-} module Development.IDE.LSP.Server ( ReactorMessage(..) , ReactorChan@@ -14,15 +10,14 @@ , requestHandler , notificationHandler ) where--import Control.Monad.IO.Unlift (MonadUnliftIO)+import Control.Monad.IO.Unlift (MonadUnliftIO) import Control.Monad.Reader import Development.IDE.Core.Shake import Development.IDE.Core.Tracing-import Ide.Types (HasTracing, traceWithSpan)-import Language.LSP.Server (Handlers, LspM)-import qualified Language.LSP.Server as LSP-import Language.LSP.Types+import Ide.Types (HasTracing, traceWithSpan)+import Language.LSP.Protocol.Message+import Language.LSP.Server (Handlers, LspM)+import qualified Language.LSP.Server as LSP import Language.LSP.VFS import UnliftIO.Chan @@ -35,25 +30,26 @@ deriving (Functor, Applicative, Monad, MonadReader (ReactorChan, IdeState), MonadIO, MonadUnliftIO, LSP.MonadLsp c) requestHandler- :: forall (m :: Method FromClient Request) c. (HasTracing (MessageParams m)) =>+ :: forall (m :: Method ClientToServer Request) c. (HasTracing (MessageParams m)) => SMethod m- -> (IdeState -> MessageParams m -> LspM c (Either ResponseError (ResponseResult m)))+ -> (IdeState -> MessageParams m -> LspM c (Either ResponseError (MessageResult m))) -> Handlers (ServerM c)-requestHandler m k = LSP.requestHandler m $ \RequestMessage{_method,_id,_params} resp -> do+requestHandler m k = LSP.requestHandler m $ \TRequestMessage{_method,_id,_params} resp -> do st@(chan,ide) <- ask env <- LSP.getLspEnv- let resp' = flip (runReaderT . unServerM) st . resp+ let resp' :: Either ResponseError (MessageResult m) -> LspM c ()+ resp' = flip (runReaderT . unServerM) st . resp trace x = otTracedHandler "Request" (show _method) $ \sp -> do traceWithSpan sp _params x writeChan chan $ ReactorRequest (SomeLspId _id) (trace $ LSP.runLspT env $ resp' =<< k ide _params) (LSP.runLspT env . resp' . Left) notificationHandler- :: forall (m :: Method FromClient Notification) c. (HasTracing (MessageParams m)) =>+ :: forall (m :: Method ClientToServer Notification) c. (HasTracing (MessageParams m)) => SMethod m -> (IdeState -> VFS -> MessageParams m -> LspM c ()) -> Handlers (ServerM c)-notificationHandler m k = LSP.notificationHandler m $ \NotificationMessage{_params,_method}-> do+notificationHandler m k = LSP.notificationHandler m $ \TNotificationMessage{_params,_method}-> do (chan,ide) <- ask env <- LSP.getLspEnv -- Take a snapshot of the VFS state on every notification
src/Development/IDE/Main.hs view
@@ -77,7 +77,7 @@ import qualified Development.IDE.Session as Session import Development.IDE.Types.Location (NormalizedUri, toNormalizedFilePath')-import Development.IDE.Types.Logger (Logger,+import Ide.Logger (Logger, Pretty (pretty), Priority (Info, Warning), Recorder,@@ -235,7 +235,7 @@ { optCheckProject = pure $ checkProject config , optCheckParents = pure $ checkParents config }- , argsLspOptions = def {LSP.completionTriggerCharacters = Just "."}+ , argsLspOptions = def {LSP.optCompletionTriggerCharacters = Just "."} , argsDefaultHlsConfig = def , argsGetHieDbLoc = getHieDbLoc , argsDebouncer = newAsyncDebouncer@@ -293,7 +293,7 @@ let hlsPlugin = asGhcIdePlugin (cmapWithPrio LogPluginHLS recorder) argsHlsPlugins hlsCommands = allLspCmdIds' pid argsHlsPlugins plugins = hlsPlugin <> argsGhcidePlugin- options = argsLspOptions { LSP.executeCommandCommands = LSP.executeCommandCommands argsLspOptions <> Just hlsCommands }+ options = argsLspOptions { LSP.optExecuteCommandCommands = LSP.optExecuteCommandCommands argsLspOptions <> Just hlsCommands } argsOnConfigChange = getConfigFromNotification argsHlsPlugins rules = argsRules >> pluginRules plugins
src/Development/IDE/Main/HeapStats.hs view
@@ -6,11 +6,11 @@ import Control.Concurrent.Async import Control.Monad import Data.Word-import Development.IDE.Types.Logger (Pretty (pretty), Priority (Info),- Recorder, WithPriority, hsep,- logWith, (<+>)) import GHC.Stats-import Text.Printf (printf)+import Ide.Logger (Pretty (pretty), Priority (Info),+ Recorder, WithPriority, hsep,+ logWith, (<+>))+import Text.Printf (printf) data Log = LogHeapStatsPeriod !Int
src/Development/IDE/Monitoring/EKG.hs view
@@ -1,14 +1,14 @@ {-# LANGUAGE CPP #-} module Development.IDE.Monitoring.EKG(monitoring) where -import Development.IDE.Types.Logger (Logger) import Development.IDE.Types.Monitoring (Monitoring (..))+import Ide.Logger (Logger) #ifdef MONITORING_EKG import Control.Concurrent (killThread) import Control.Concurrent.Async (async, waitCatch) import Control.Monad (forM_) import Data.Text (pack)-import Development.IDE.Types.Logger (logInfo)+import Ide.Logger (logInfo) import qualified System.Metrics as Monitoring import qualified System.Remote.Monitoring.Wai as Monitoring
src/Development/IDE/Plugin/Completions.hs view
@@ -11,15 +11,18 @@ import Control.Concurrent.Async (concurrently) import Control.Concurrent.STM.Stats (readTVarIO)+import Control.Lens ((&), (.~)) import Control.Monad.IO.Class-import Control.Lens ((&), (.~))+import Control.Monad.Trans.Except (ExceptT (ExceptT),+ withExceptT)+import Data.Aeson import qualified Data.HashMap.Strict as Map import qualified Data.HashSet as Set-import Data.Aeson import Data.Maybe import qualified Data.Text as T-import Development.IDE.Core.PositionMapping import Development.IDE.Core.Compile+import Development.IDE.Core.PluginUtils+import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes import Development.IDE.Core.Service hiding (Log, LogShake) import Development.IDE.Core.Shake hiding (Log)@@ -27,24 +30,25 @@ import Development.IDE.GHC.Compat import Development.IDE.GHC.Util import Development.IDE.Graph-import Development.IDE.Spans.Common-import Development.IDE.Spans.Documentation import Development.IDE.Plugin.Completions.Logic import Development.IDE.Plugin.Completions.Types+import Development.IDE.Spans.Common+import Development.IDE.Spans.Documentation import Development.IDE.Types.Exports import Development.IDE.Types.HscEnvEq (HscEnvEq (envPackageExports, envVisibleModuleNames), hscEnv) import qualified Development.IDE.Types.KnownTargets as KT import Development.IDE.Types.Location-import Development.IDE.Types.Logger (Pretty (pretty),+import Ide.Logger (Pretty (pretty), Recorder, WithPriority, cmapWithPrio)+import Ide.Plugin.Error import Ide.Types+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types import qualified Language.LSP.Server as LSP-import Language.LSP.Types-import qualified Language.LSP.Types.Lens as J-import qualified Language.LSP.VFS as VFS import Numeric.Natural import Text.Fuzzy.Parallel (Scored (..)) @@ -64,8 +68,8 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = (defaultPluginDescriptor plId) { pluginRules = produceCompletions recorder- , pluginHandlers = mkPluginHandler STextDocumentCompletion getCompletionsLSP- <> mkPluginHandler SCompletionItemResolve resolveCompletion+ , pluginHandlers = mkPluginHandler SMethod_TextDocumentCompletion getCompletionsLSP+ <> mkResolveHandler SMethod_CompletionItemResolve resolveCompletion , pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} , pluginPriority = ghcideCompletionsPluginPriority }@@ -118,59 +122,50 @@ f x = x in f <$> iDecl -resolveCompletion :: IdeState -> PluginId -> CompletionItem -> LSP.LspM Config (Either ResponseError CompletionItem)-resolveCompletion ide _ comp@CompletionItem{_detail,_documentation,_xdata}- | Just resolveData <- _xdata- , Success (CompletionResolveData uri needType (NameDetails mod occ)) <- fromJSON resolveData- , Just file <- uriToNormalizedFilePath $ toNormalizedUri uri- = liftIO $ runIdeAction "Completion resolve" (shakeExtras ide) $ do- msess <- useWithStaleFast GhcSessionDeps file- case msess of- Nothing -> pure (Right comp) -- File doesn't compile, return original completion item- Just (sess,_) -> do- let nc = ideNc $ shakeExtras ide+resolveCompletion :: ResolveFunction IdeState CompletionResolveData 'Method_CompletionItemResolve+resolveCompletion ide _pid comp@CompletionItem{_detail,_documentation,_data_} uri (CompletionResolveData _ needType (NameDetails mod occ)) =+ do+ file <- getNormalizedFilePathE uri+ (sess,_) <- withExceptT (const PluginStaleResolve)+ $ runIdeActionE "CompletionResolve.GhcSessionDeps" (shakeExtras ide)+ $ useWithStaleFastE GhcSessionDeps file+ let nc = ideNc $ shakeExtras ide #if MIN_VERSION_ghc(9,3,0)- name <- liftIO $ lookupNameCache nc mod occ+ name <- liftIO $ lookupNameCache nc mod occ #else- name <- liftIO $ upNameCache nc (lookupNameCache mod occ)+ name <- liftIO $ upNameCache nc (lookupNameCache mod occ) #endif- mdkm <- useWithStaleFast GetDocMap file- let (dm,km) = case mdkm of- Just (DKMap dm km, _) -> (dm,km)- Nothing -> (mempty, mempty)- doc <- case lookupNameEnv dm name of- Just doc -> pure $ spanDocToMarkdown doc- Nothing -> liftIO $ spanDocToMarkdown <$> getDocumentationTryGhc (hscEnv sess) name- typ <- case lookupNameEnv km name of- _ | not needType -> pure Nothing- Just ty -> pure (safeTyThingType ty)- Nothing -> do- (safeTyThingType =<<) <$> liftIO (lookupName (hscEnv sess) name)- let det1 = case typ of- Just ty -> Just (":: " <> printOutputable (stripForall ty) <> "\n")- Nothing -> Nothing- doc1 = case _documentation of- Just (CompletionDocMarkup (MarkupContent MkMarkdown old)) ->- CompletionDocMarkup $ MarkupContent MkMarkdown $ T.intercalate sectionSeparator (old:doc)- _ -> CompletionDocMarkup $ MarkupContent MkMarkdown $ T.intercalate sectionSeparator doc- pure (Right $ comp & J.detail .~ (det1 <> _detail)- & J.documentation .~ Just doc1- )+ mdkm <- liftIO $ runIdeAction "CompletionResolve.GetDocMap" (shakeExtras ide) $ useWithStaleFast GetDocMap file+ let (dm,km) = case mdkm of+ Just (DKMap dm km, _) -> (dm,km)+ Nothing -> (mempty, mempty)+ doc <- case lookupNameEnv dm name of+ Just doc -> pure $ spanDocToMarkdown doc+ Nothing -> liftIO $ spanDocToMarkdown <$> getDocumentationTryGhc (hscEnv sess) name+ typ <- case lookupNameEnv km name of+ _ | not needType -> pure Nothing+ Just ty -> pure (safeTyThingType ty)+ Nothing -> do+ (safeTyThingType =<<) <$> liftIO (lookupName (hscEnv sess) name)+ let det1 = case typ of+ Just ty -> Just (":: " <> printOutputable (stripForall ty) <> "\n")+ Nothing -> Nothing+ doc1 = case _documentation of+ Just (InR (MarkupContent MarkupKind_Markdown old)) ->+ InR $ MarkupContent MarkupKind_Markdown $ T.intercalate sectionSeparator (old:doc)+ _ -> InR $ MarkupContent MarkupKind_Markdown $ T.intercalate sectionSeparator doc+ pure (comp & L.detail .~ (det1 <> _detail)+ & L.documentation .~ Just doc1) where stripForall ty = case splitForAllTyCoVars ty of (_,res) -> res-resolveCompletion _ _ comp = pure (Right comp) -- | Generate code actions.-getCompletionsLSP- :: IdeState- -> PluginId- -> CompletionParams- -> LSP.LspM Config (Either ResponseError (ResponseResult TextDocumentCompletion))+getCompletionsLSP :: PluginMethodHandler IdeState 'Method_TextDocumentCompletion getCompletionsLSP ide plId CompletionParams{_textDocument=TextDocumentIdentifier uri ,_position=position- ,_context=completionContext} = do+ ,_context=completionContext} = ExceptT $ do contents <- LSP.getVirtualFile $ toNormalizedUri uri fmap Right $ case (contents, uriToFilePath' uri) of (Just cnts, Just path) -> do@@ -213,17 +208,16 @@ let pfix = getCompletionPrefix position cnts case (pfix, completionContext) of ((PosPrefixInfo _ "" _ _), Just CompletionContext { _triggerCharacter = Just "."})- -> return (InL $ List [])+ -> return (InL []) (_, _) -> do let clientCaps = clientCapabilities $ shakeExtras ide plugins = idePlugins $ shakeExtras ide config <- liftIO $ runAction "" ide $ getCompletionsConfig plId allCompletions <- liftIO $ getCompletions plugins ideOpts cci' parsedMod astres bindMap pfix clientCaps config moduleExports uri- pure $ InL (List $ orderedCompletions allCompletions)- _ -> return (InL $ List [])- _ -> return (InL $ List [])- _ -> return (InL $ List [])+ pure $ InL (orderedCompletions allCompletions)+ _ -> return (InL [])+ _ -> return (InL []) getCompletionsConfig :: PluginId -> Action CompletionsConfig getCompletionsConfig pId =
src/Development/IDE/Plugin/Completions/Logic.hs view
@@ -1,7 +1,8 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE MultiWayIf #-}-+{-# LANGUAGE CPP #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedLabels #-} -- Mostly taken from "haskell-ide-engine" module Development.IDE.Plugin.Completions.Logic (@@ -14,15 +15,19 @@ ) where import Control.Applicative+import Control.Lens hiding (Context) import Data.Char (isAlphaNum, isUpper)+import Data.Default (def) import Data.Generics import Data.List.Extra as List hiding (stripPrefix) import qualified Data.Map as Map+import Data.Row import Data.Maybe (catMaybes, fromMaybe,- isJust, listToMaybe,- mapMaybe, isNothing)+ isJust, isNothing,+ listToMaybe,+ mapMaybe) import qualified Data.Text as T import qualified Text.Fuzzy.Parallel as Fuzzy @@ -41,7 +46,7 @@ import Development.IDE.GHC.Compat hiding (ppr) import qualified Development.IDE.GHC.Compat as GHC import Development.IDE.GHC.Compat.Util-import Development.IDE.GHC.CoreFile (occNamePrefixes)+import Development.IDE.GHC.CoreFile (occNamePrefixes) import Development.IDE.GHC.Error import Development.IDE.GHC.Util import Development.IDE.Plugin.Completions.Types@@ -64,8 +69,8 @@ import Ide.Types (CommandId (..), IdePlugins (..), PluginId)-import Language.LSP.Types-import Language.LSP.Types.Capabilities+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Types import qualified Language.LSP.VFS as VFS import Text.Fuzzy.Parallel (Scored (score), original)@@ -76,7 +81,7 @@ import Development.IDE.Spans.AtPoint (pointCommand) #if MIN_VERSION_ghc(9,5,0)-import Language.Haskell.Syntax.Basic+import Language.Haskell.Syntax.Basic #endif -- Chunk size used for parallelizing fuzzy matching@@ -172,11 +177,11 @@ occNameToComKind :: OccName -> CompletionItemKind occNameToComKind oc | isVarOcc oc = case occNameString oc of- i:_ | isUpper i -> CiConstructor- _ -> CiFunction- | isTcOcc oc = CiStruct- | isDataOcc oc = CiConstructor- | otherwise = CiVariable+ i:_ | isUpper i -> CompletionItemKind_Constructor+ _ -> CompletionItemKind_Function+ | isTcOcc oc = CompletionItemKind_Struct+ | isDataOcc oc = CompletionItemKind_Constructor+ | otherwise = CompletionItemKind_Variable showModName :: ModuleName -> T.Text@@ -215,13 +220,15 @@ _sortText = Nothing, _filterText = Nothing, _insertText = Just insertText,- _insertTextFormat = Just Snippet,+ _insertTextFormat = Just InsertTextFormat_Snippet, _insertTextMode = Nothing, _textEdit = Nothing, _additionalTextEdits = Nothing, _commitCharacters = Nothing, _command = mbCommand,- _xdata = toJSON <$> fmap (CompletionResolveData uri (isNothing typeText)) nameDetails}+ _data_ = toJSON <$> fmap (CompletionResolveData uri (isNothing typeText)) nameDetails,+ _labelDetails = Nothing,+ _textEditText = Nothing} removeSnippetsWhen (isJust isInfix) ci where kind = Just compKind@@ -230,8 +237,8 @@ Local pos -> "*Defined at " <> pprLineCol (srcSpanStart pos) <> " in this module*\n" ImportedFrom mod -> "*Imported from '" <> mod <> "'*\n" DefinedIn mod -> "*Defined in '" <> mod <> "'*\n"- documentation = Just $ CompletionDocMarkup $- MarkupContent MkMarkdown $+ documentation = Just $ InR $+ MarkupContent MarkupKind_Markdown $ T.intercalate sectionSeparator docs' pprLineCol :: SrcLoc -> T.Text pprLineCol (UnhelpfulLoc fs) = T.pack $ unpackFS fs@@ -253,8 +260,8 @@ typeText = Nothing label = stripPrefix $ printOutputable origName insertText = case isInfix of- Nothing -> label- Just LeftSide -> label <> "`"+ Nothing -> label+ Just LeftSide -> label <> "`" Just Surrounded -> label additionalTextEdits =@@ -278,30 +285,32 @@ mkModCompl :: T.Text -> CompletionItem mkModCompl label =- CompletionItem label (Just CiModule) Nothing Nothing- Nothing Nothing Nothing Nothing Nothing Nothing Nothing- Nothing Nothing Nothing Nothing Nothing Nothing+ (defaultCompletionItemWithLabel label)+ { _kind = Just CompletionItemKind_Module } mkModuleFunctionImport :: T.Text -> T.Text -> CompletionItem mkModuleFunctionImport moduleName label =- CompletionItem label (Just CiFunction) Nothing (Just moduleName)- Nothing Nothing Nothing Nothing Nothing Nothing Nothing- Nothing Nothing Nothing Nothing Nothing Nothing+ (defaultCompletionItemWithLabel label)+ { _kind = Just CompletionItemKind_Function+ , _detail = Just moduleName } mkImportCompl :: T.Text -> T.Text -> CompletionItem mkImportCompl enteredQual label =- CompletionItem m (Just CiModule) Nothing (Just label)- Nothing Nothing Nothing Nothing Nothing Nothing Nothing- Nothing Nothing Nothing Nothing Nothing Nothing+ (defaultCompletionItemWithLabel m)+ { _kind = Just CompletionItemKind_Module+ , _detail = Just label } where m = fromMaybe "" (T.stripPrefix enteredQual label) mkExtCompl :: T.Text -> CompletionItem mkExtCompl label =- CompletionItem label (Just CiKeyword) Nothing Nothing- Nothing Nothing Nothing Nothing Nothing Nothing Nothing- Nothing Nothing Nothing Nothing Nothing Nothing+ (defaultCompletionItemWithLabel label)+ { _kind = Just CompletionItemKind_Keyword } +defaultCompletionItemWithLabel :: T.Text -> CompletionItem+defaultCompletionItemWithLabel label =+ CompletionItem label def def def def def def def def def+ def def def def def def def def def fromIdentInfo :: Uri -> IdentInfo -> Maybe T.Text -> CompItem fromIdentInfo doc id@IdentInfo{..} q = CI@@ -439,20 +448,20 @@ compls = concat [ case decl of SigD _ (TypeSig _ ids typ) ->- [mkComp id CiFunction (Just $ showForSnippet typ) | id <- ids]+ [mkComp id CompletionItemKind_Function (Just $ showForSnippet typ) | id <- ids] ValD _ FunBind{fun_id} ->- [ mkComp fun_id CiFunction Nothing+ [ mkComp fun_id CompletionItemKind_Function Nothing | not (hasTypeSig fun_id) ] ValD _ PatBind{pat_lhs} ->- [mkComp id CiVariable Nothing+ [mkComp id CompletionItemKind_Variable Nothing | VarPat _ id <- listify (\(_ :: Pat GhcPs) -> True) pat_lhs] TyClD _ ClassDecl{tcdLName, tcdSigs, tcdATs} ->- mkComp tcdLName CiInterface (Just $ showForSnippet tcdLName) :- [ mkComp id CiFunction (Just $ showForSnippet typ)+ mkComp tcdLName CompletionItemKind_Interface (Just $ showForSnippet tcdLName) :+ [ mkComp id CompletionItemKind_Function (Just $ showForSnippet typ) | L _ (ClassOpSig _ _ ids typ) <- tcdSigs , id <- ids] ++- [ mkComp fdLName CiStruct (Just $ showForSnippet fdLName)+ [ mkComp fdLName CompletionItemKind_Struct (Just $ showForSnippet fdLName) | L _ (FamilyDecl{fdLName}) <- tcdATs] TyClD _ x -> let generalCompls = [mkComp id cl (Just $ showForSnippet $ tyClDeclLName x)@@ -464,16 +473,16 @@ -- the constructors and snippets will be duplicated here giving the user 2 choices. generalCompls ++ recordCompls ForD _ ForeignImport{fd_name,fd_sig_ty} ->- [mkComp fd_name CiVariable (Just $ showForSnippet fd_sig_ty)]+ [mkComp fd_name CompletionItemKind_Variable (Just $ showForSnippet fd_sig_ty)] ForD _ ForeignExport{fd_name,fd_sig_ty} ->- [mkComp fd_name CiVariable (Just $ showForSnippet fd_sig_ty)]+ [mkComp fd_name CompletionItemKind_Variable (Just $ showForSnippet fd_sig_ty)] _ -> [] | L (locA -> pos) decl <- hsmodDecls, let mkComp = mkLocalComp pos ] mkLocalComp pos n ctyp ty =- CI ctyp pn (Local pos) pn ty Nothing (ctyp `elem` [CiStruct, CiInterface]) Nothing (Just $ NameDetails (ms_mod $ pm_mod_summary pm) occ) True+ CI ctyp pn (Local pos) pn ty Nothing (ctyp `elem` [CompletionItemKind_Struct, CompletionItemKind_Interface]) Nothing (Just $ NameDetails (ms_mod $ pm_mod_summary pm) occ) True where occ = rdrNameOcc $ unLoc n pn = showForSnippet n@@ -520,7 +529,7 @@ removeSnippetsWhen (not $ enableSnippets && supported) where supported =- Just True == (_textDocument >>= _completion >>= _completionItem >>= _snippetSupport)+ Just True == (_textDocument >>= _completion >>= view L.completionItem >>= (\x -> x .! #snippetSupport)) toggleAutoExtend :: CompletionsConfig -> CompItem -> CompItem toggleAutoExtend CompletionsConfig{enableAutoExtend=False} x = x {additionalTextEdits = Nothing}@@ -531,7 +540,7 @@ if condition then x- { _insertTextFormat = Just PlainText,+ { _insertTextFormat = Just InsertTextFormat_PlainText, _insertText = Nothing } else x@@ -613,7 +622,7 @@ -- to get the record's module, which isn't included in the type information used to get the fields. dotFieldSelectorToCompl :: T.Text -> T.Text -> (Bool, CompItem) dotFieldSelectorToCompl recname label = (True, CI- { compKind = CiField+ { compKind = CompletionItemKind_Field , insertText = label , provenance = DefinedIn recname , label = label@@ -790,7 +799,7 @@ mkRecordSnippetCompItem uri parent ctxStr compl importedFrom imp = r where r = CI {- compKind = CiSnippet+ compKind = CompletionItemKind_Snippet , insertText = buildSnippet , provenance = importedFrom , typeText = Nothing
src/Development/IDE/Plugin/Completions/Types.hs view
@@ -1,9 +1,9 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE CPP #-} module Development.IDE.Plugin.Completions.Types ( module Development.IDE.Plugin.Completions.Types ) where@@ -22,12 +22,12 @@ import Development.IDE.Spans.Common import GHC.Generics (Generic) import Ide.Plugin.Properties-import Language.LSP.Types (CompletionItemKind (..), Uri)-import qualified Language.LSP.Types as J+import Language.LSP.Protocol.Types (CompletionItemKind (..), Uri)+import qualified Language.LSP.Protocol.Types as J #if MIN_VERSION_ghc(9,0,0)-import qualified GHC.Types.Name.Occurrence as Occ+import qualified GHC.Types.Name.Occurrence as Occ #else-import qualified OccName as Occ+import qualified OccName as Occ #endif -- | Produce completions info for a file@@ -178,7 +178,7 @@ parseNs (String "c") = pure dataName parseNs (String "t") = pure tcClsName parseNs (String "z") = pure tvName-parseNs _ = mempty+parseNs _ = mempty instance FromJSON NameDetails where parseJSON v@(Array _)@@ -204,9 +204,9 @@ -- We need the URI to be able to reconstruct the GHC environment -- in the file the completion was triggered in. data CompletionResolveData = CompletionResolveData- { itemFile :: Uri+ { itemFile :: Uri , itemNeedsType :: Bool -- ^ Do we need to lookup a type for this item?- , itemName :: NameDetails+ , itemName :: NameDetails } deriving stock Generic deriving anyclass (FromJSON, ToJSON)
src/Development/IDE/Plugin/HLS.hs view
@@ -6,75 +6,92 @@ module Development.IDE.Plugin.HLS ( asGhcIdePlugin+ , toResponseError , Log(..) ) where -import Control.Exception (SomeException)-import Control.Lens ((^.))+import Control.Exception (SomeException)+import Control.Lens ((^.)) import Control.Monad-import qualified Data.Aeson as J-import Data.Bifunctor (first)-import Data.Dependent.Map (DMap)-import qualified Data.Dependent.Map as DMap+import Control.Monad.Trans.Except (runExceptT)+import qualified Data.Aeson as A+import Data.Bifunctor (first)+import Data.Dependent.Map (DMap)+import qualified Data.Dependent.Map as DMap import Data.Dependent.Sum import Data.Either-import qualified Data.List as List-import Data.List.NonEmpty (NonEmpty, nonEmpty, toList)-import qualified Data.List.NonEmpty as NE-import qualified Data.Map as Map+import qualified Data.List as List+import Data.List.NonEmpty (NonEmpty, nonEmpty, toList)+import qualified Data.List.NonEmpty as NE+import qualified Data.Map as Map import Data.Some import Data.String-import Data.Text (Text)-import qualified Data.Text as T-import Development.IDE.Core.Shake hiding (Log)+import Data.Text (Text)+import qualified Data.Text as T+import Development.IDE.Core.Shake hiding (Log) import Development.IDE.Core.Tracing-import Development.IDE.Graph (Rules)+import Development.IDE.Graph (Rules) import Development.IDE.LSP.Server import Development.IDE.Plugin-import qualified Development.IDE.Plugin as P-import Development.IDE.Types.Logger+import qualified Development.IDE.Plugin as P+import Ide.Logger import Ide.Plugin.Config-import Ide.PluginUtils (getClientConfig)-import Ide.Types as HLS-import qualified Language.LSP.Server as LSP-import Language.LSP.Types-import qualified Language.LSP.Types as J-import qualified Language.LSP.Types.Lens as LSP+import Ide.Plugin.Error+import Ide.PluginUtils (getClientConfig)+import Ide.Types as HLS+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types+import qualified Language.LSP.Server as LSP import Language.LSP.VFS-import Prettyprinter.Render.String (renderString)-import Text.Regex.TDFA.Text ()-import UnliftIO (MonadUnliftIO)-import UnliftIO.Async (forConcurrently)-import UnliftIO.Exception (catchAny)+import Prettyprinter.Render.String (renderString)+import Text.Regex.TDFA.Text ()+import UnliftIO (MonadUnliftIO, liftIO)+import UnliftIO.Async (forConcurrently)+import UnliftIO.Exception (catchAny) -- --------------------------------------------------------------------- -- data Log- = LogPluginError PluginId ResponseError+ = LogPluginError PluginId PluginError+ | LogResponseError PluginId ResponseError | LogNoPluginForMethod (Some SMethod) | LogInvalidCommandIdentifier+ | ExceptionInPlugin PluginId (Some SMethod) SomeException+ instance Pretty Log where pretty = \case- LogPluginError (PluginId pId) err -> pretty pId <> ":" <+> prettyResponseError err+ LogPluginError (PluginId pId) err ->+ pretty pId <> ":" <+> pretty err+ LogResponseError (PluginId pId) err ->+ pretty pId <> ":" <+> prettyResponseError err LogNoPluginForMethod (Some method) -> "No plugin enabled for " <> pretty (show method) LogInvalidCommandIdentifier-> "Invalid command identifier"-+ ExceptionInPlugin plId (Some method) exception ->+ "Exception in plugin " <> viaShow plId <> " while processing "+ <> viaShow method <> ": " <> viaShow exception instance Show Log where show = renderString . layoutCompact . pretty -- various error message specific builders prettyResponseError :: ResponseError -> Doc a prettyResponseError err = errorCode <> ":" <+> errorBody where- errorCode = pretty $ show $ err ^. LSP.code- errorBody = pretty $ err ^. LSP.message--pluginNotEnabled :: SMethod m -> [(PluginId, b, a)] -> Text-pluginNotEnabled method availPlugins =- "No plugin enabled for " <> T.pack (show method) <> ", available: "- <> (T.intercalate ", " $ map (\(PluginId plid, _, _) -> plid) availPlugins)+ errorCode = pretty $ show $ err ^. L.code+ errorBody = pretty $ err ^. L.message +noPluginEnabled :: Recorder (WithPriority Log) -> SMethod m -> [PluginId] -> IO (Either ResponseError c)+noPluginEnabled recorder m fs' = do+ logWith recorder Warning (LogNoPluginForMethod $ Some m)+ let err = ResponseError (InR ErrorCodes_MethodNotFound) msg Nothing+ msg = pluginNotEnabled m fs'+ return $ Left err+ where pluginNotEnabled :: SMethod m -> [PluginId] -> Text+ pluginNotEnabled method availPlugins =+ "No plugin enabled for " <> T.pack (show method) <> ", potentially available: "+ <> (T.intercalate ", " $ map (\(PluginId plid) -> plid) availPlugins)+ pluginDoesntExist :: PluginId -> Text pluginDoesntExist (PluginId pid) = "Plugin " <> pid <> " doesn't exist" @@ -86,17 +103,21 @@ failedToParseArgs :: CommandId -- ^ command that failed to parse -> PluginId -- ^ Plugin that created the command -> String -- ^ The JSON Error message- -> J.Value -- ^ The Argument Values+ -> A.Value -- ^ The Argument Values -> Text failedToParseArgs (CommandId com) (PluginId pid) err arg = "Error while parsing args for " <> com <> " in plugin " <> pid <> ": " <> T.pack err <> ", arg = " <> T.pack (show arg) +exceptionInPlugin :: PluginId -> SMethod m -> SomeException -> Text+exceptionInPlugin plId method exception =+ "Exception in plugin " <> T.pack (show plId) <> " while processing "<> T.pack (show method) <> ": " <> T.pack (show exception)+ -- | Build a ResponseError and log it before returning to the caller-logAndReturnError :: Recorder (WithPriority Log) -> PluginId -> ErrorCode -> Text -> LSP.LspT Config IO (Either ResponseError a)+logAndReturnError :: Recorder (WithPriority Log) -> PluginId -> (LSPErrorCodes |? ErrorCodes) -> Text -> LSP.LspT Config IO (Either ResponseError a) logAndReturnError recorder p errCode msg = do let err = ResponseError errCode msg Nothing- logWith recorder Warning $ LogPluginError p err+ logWith recorder Warning $ LogResponseError p err pure $ Left err -- | Map a set of plugins to the underlying ghcide engine.@@ -146,7 +167,7 @@ executeCommandPlugins recorder ecs = mempty { P.pluginHandlers = executeCommandHandlers recorder ecs } executeCommandHandlers :: Recorder (WithPriority Log) -> [(PluginId, [PluginCommand IdeState])] -> LSP.Handlers (ServerM Config)-executeCommandHandlers recorder ecs = requestHandler SWorkspaceExecuteCommand execCmd+executeCommandHandlers recorder ecs = requestHandler SMethod_WorkspaceExecuteCommand execCmd where pluginMap = Map.fromListWith (++) ecs @@ -157,29 +178,29 @@ _ -> Nothing -- The parameters to the HLS command are always the first element-+ execCmd :: IdeState -> ExecuteCommandParams -> LSP.LspT Config IO (Either ResponseError (A.Value |? Null)) execCmd ide (ExecuteCommandParams _ cmdId args) = do- let cmdParams :: J.Value+ let cmdParams :: A.Value cmdParams = case args of- Just (J.List (x:_)) -> x- _ -> J.Null+ Just ((x:_)) -> x+ _ -> A.Null case parseCmdId cmdId of -- Shortcut for immediately applying a applyWorkspaceEdit as a fallback for v3.8 code actions Just ("hls", "fallbackCodeAction") ->- case J.fromJSON cmdParams of- J.Success (FallbackCodeActionParams mEdit mCmd) -> do+ case A.fromJSON cmdParams of+ A.Success (FallbackCodeActionParams mEdit mCmd) -> do -- Send off the workspace request if it has one forM_ mEdit $ \edit ->- LSP.sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ())+ LSP.sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) case mCmd of -- If we have a command, continue to execute it- Just (J.Command _ innerCmdId innerArgs)+ Just (Command _ innerCmdId innerArgs) -> execCmd ide (ExecuteCommandParams Nothing innerCmdId innerArgs)- Nothing -> return $ Right J.Null+ Nothing -> return $ Right $ InR Null - J.Error _str -> return $ Right J.Null+ A.Error _str -> return $ Right $ InR Null -- Just an ordinary HIE command Just (plugin, cmd) -> runPluginCommand ide plugin cmd cmdParams@@ -187,16 +208,26 @@ -- Couldn't parse the command identifier _ -> do logWith recorder Warning LogInvalidCommandIdentifier- return $ Left $ ResponseError InvalidParams "Invalid command identifier" Nothing+ return $ Left $ ResponseError (InR ErrorCodes_InvalidParams) "Invalid command identifier" Nothing + runPluginCommand :: IdeState -> PluginId -> CommandId -> A.Value -> LSP.LspT Config IO (Either ResponseError (A.Value |? Null)) runPluginCommand ide p com arg = case Map.lookup p pluginMap of- Nothing -> logAndReturnError recorder p InvalidRequest (pluginDoesntExist p)+ Nothing -> logAndReturnError recorder p (InR ErrorCodes_InvalidRequest) (pluginDoesntExist p) Just xs -> case List.find ((com ==) . commandId) xs of- Nothing -> logAndReturnError recorder p InvalidRequest (commandDoesntExist com p xs)- Just (PluginCommand _ _ f) -> case J.fromJSON arg of- J.Error err -> logAndReturnError recorder p InvalidParams (failedToParseArgs com p err arg)- J.Success a -> f ide a+ Nothing -> logAndReturnError recorder p (InR ErrorCodes_InvalidRequest) (commandDoesntExist com p xs)+ Just (PluginCommand _ _ f) -> case A.fromJSON arg of+ A.Error err -> logAndReturnError recorder p (InR ErrorCodes_InvalidParams) (failedToParseArgs com p err arg)+ A.Success a -> do+ res <- runExceptT (f ide a) `catchAny` -- See Note [Exception handling in plugins]+ (\e -> pure $ Left $ PluginInternalError (exceptionInPlugin p SMethod_WorkspaceExecuteCommand e))+ case res of+ (Left (PluginRequestRefused _)) ->+ liftIO $ noPluginEnabled recorder SMethod_WorkspaceExecuteCommand (fst <$> ecs)+ (Left pluginErr) -> do+ liftIO $ logErrors recorder [(p, pluginErr)]+ pure $ Left $ toResponseError (p, pluginErr)+ (Right result) -> pure $ Right result -- --------------------------------------------------------------------- @@ -218,25 +249,25 @@ let fs = filter (\(_, desc, _) -> pluginEnabled m params desc config) fs' -- Clients generally don't display ResponseErrors so instead we log any that we come across case nonEmpty fs of- Nothing -> do- logWith recorder Warning (LogNoPluginForMethod $ Some m)- let err = ResponseError InvalidRequest msg Nothing- msg = pluginNotEnabled m fs'- return $ Left err+ Nothing -> liftIO $ noPluginEnabled recorder m ((\(x, _, _) -> x) <$> fs') Just fs -> do- let msg e pid = "Exception in plugin " <> T.pack (show pid) <> " while processing " <> T.pack (show m) <> ": " <> T.pack (show e)- handlers = fmap (\(plid,_,handler) -> (plid,handler)) fs- es <- runConcurrently msg (show m) handlers ide params-+ let handlers = fmap (\(plid,_,handler) -> (plid,handler)) fs+ es <- runConcurrently exceptionInPlugin m handlers ide params+ caps <- LSP.getClientCapabilities let (errs,succs) = partitionEithers $ toList $ join $ NE.zipWith (\(pId,_) -> fmap (first (pId,))) handlers es- unless (null errs) $ forM_ errs $ \(pId, err) ->- logWith recorder Warning $ LogPluginError pId err+ liftIO $ unless (null errs) $ logErrors recorder errs case nonEmpty succs of- Nothing -> pure $ Left $ combineErrors $ map snd errs+ Nothing -> do+ let noRefused (_, PluginRequestRefused _) = False+ noRefused (_, _) = True+ filteredErrs = filter noRefused errs+ case nonEmpty filteredErrs of+ Nothing -> liftIO $ noPluginEnabled recorder m ((\(x, _, _) -> x) <$> fs')+ Just xs -> pure $ Left $ combineErrors xs Just xs -> do- caps <- LSP.getClientCapabilities pure $ Right $ combineResponses m config caps params xs + -- --------------------------------------------------------------------- extensibleNotificationPlugins :: Recorder (WithPriority Log) -> [(PluginId, PluginDescriptor IdeState)] -> Plugin Config@@ -260,35 +291,51 @@ Just fs -> do -- We run the notifications in order, so the core ghcide provider -- (which restarts the shake process) hopefully comes last- mapM_ (\(pid,_,f) -> otTracedProvider pid (fromString $ show m) $ f ide vfs params) fs+ mapM_ (\(pid,_,f) -> otTracedProvider pid (fromString $ show m) $ f ide vfs params+ `catchAny` -- See Note [Exception handling in plugins]+ (\e -> logWith recorder Warning (ExceptionInPlugin pid (Some m) e))) fs + -- --------------------------------------------------------------------- runConcurrently :: MonadUnliftIO m- => (SomeException -> PluginId -> T.Text)- -> String -- ^ label- -> NonEmpty (PluginId, a -> b -> m (NonEmpty (Either ResponseError d)))+ => (PluginId -> SMethod method -> SomeException -> T.Text)+ -> SMethod method -- ^ Method (used for errors and tracing)+ -> NonEmpty (PluginId, a -> b -> m (NonEmpty (Either PluginError d))) -- ^ Enabled plugin actions that we are allowed to run -> a -> b- -> m (NonEmpty(NonEmpty (Either ResponseError d)))-runConcurrently msg method fs a b = forConcurrently fs $ \(pid,f) -> otTracedProvider pid (fromString method) $ do- f a b- `catchAny` (\e -> pure $ pure $ Left $ ResponseError InternalError (msg e pid) Nothing)+ -> m (NonEmpty(NonEmpty (Either PluginError d)))+runConcurrently msg method fs a b = forConcurrently fs $ \(pid,f) -> otTracedProvider pid (fromString (show method)) $ do+ f a b -- See Note [Exception handling in plugins]+ `catchAny` (\e -> pure $ pure $ Left $ PluginInternalError (msg pid method e)) -combineErrors :: [ResponseError] -> ResponseError-combineErrors [x] = x-combineErrors xs = ResponseError InternalError (T.pack (show xs)) Nothing+combineErrors :: NonEmpty (PluginId, PluginError) -> ResponseError+combineErrors (x NE.:| []) = toResponseError x+combineErrors xs = toResponseError $ NE.last $ NE.sortWith (toPriority . snd) xs +toResponseError :: (PluginId, PluginError) -> ResponseError+toResponseError (PluginId plId, err) =+ ResponseError (toErrorCode err) (plId <> ": " <> tPretty err) Nothing+ where tPretty = T.pack . show . pretty++logErrors :: Recorder (WithPriority Log) -> [(PluginId, PluginError)] -> IO ()+logErrors recorder errs = do+ forM_ errs $ \(pId, err) ->+ logIndividualErrors pId err+ where logIndividualErrors plId err =+ logWith recorder (toPriority err) $ LogPluginError plId err++ -- | Combine the 'PluginHandler' for all plugins-newtype IdeHandler (m :: J.Method FromClient Request)- = IdeHandler [(PluginId, PluginDescriptor IdeState, IdeState -> MessageParams m -> LSP.LspM Config (NonEmpty (Either ResponseError (ResponseResult m))))]+newtype IdeHandler (m :: Method ClientToServer Request)+ = IdeHandler [(PluginId, PluginDescriptor IdeState, IdeState -> MessageParams m -> LSP.LspM Config (NonEmpty (Either PluginError (MessageResult m))))] -- | Combine the 'PluginHandler' for all plugins-newtype IdeNotificationHandler (m :: J.Method FromClient Notification)+newtype IdeNotificationHandler (m :: Method ClientToServer Notification) = IdeNotificationHandler [(PluginId, PluginDescriptor IdeState, IdeState -> VFS -> MessageParams m -> LSP.LspM Config ())]--- type NotificationHandler (m :: Method FromClient Notification) = MessageParams m -> IO ()`+-- type NotificationHandler (m :: Method ClientToServer Notification) = MessageParams m -> IO ()` -- | Combine the 'PluginHandlers' for all plugins newtype IdeHandlers = IdeHandlers (DMap IdeMethod IdeHandler)@@ -307,3 +354,16 @@ go _ (IdeNotificationHandler a) (IdeNotificationHandler b) = IdeNotificationHandler (a <> b) instance Monoid IdeNotificationHandlers where mempty = IdeNotificationHandlers mempty++{- Note [Exception handling in plugins]+Plugins run in LspM, and so have access to IO. This means they are likely to+throw exceptions, even if only by accident or through calling libraries that+throw exceptions. Ultimately, we're running a bunch of less-trusted IO code,+so we should be robust to it throwing.++We don't want these to bring down HLS. So we catch and log exceptions wherever+we run a handler defined in a plugin.++The flip side of this is that it's okay for plugins to throw exceptions as a+way of signalling failure!+-}
src/Development/IDE/Plugin/HLS/GhcIde.hs view
@@ -15,8 +15,8 @@ import qualified Development.IDE.Plugin.Completions as Completions import qualified Development.IDE.Plugin.TypeLenses as TypeLenses import Ide.Types-import Language.LSP.Server (LspM)-import Language.LSP.Types+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types import Text.Regex.TDFA.Text () data Log@@ -43,29 +43,23 @@ descriptor :: PluginId -> PluginDescriptor IdeState descriptor plId = (defaultPluginDescriptor plId)- { pluginHandlers = mkPluginHandler STextDocumentHover hover'- <> mkPluginHandler STextDocumentDocumentSymbol symbolsProvider- <> mkPluginHandler STextDocumentDefinition (\ide _ DefinitionParams{..} ->+ { pluginHandlers = mkPluginHandler SMethod_TextDocumentHover hover'+ <> mkPluginHandler SMethod_TextDocumentDocumentSymbol moduleOutline+ <> mkPluginHandler SMethod_TextDocumentDefinition (\ide _ DefinitionParams{..} -> gotoDefinition ide TextDocumentPositionParams{..})- <> mkPluginHandler STextDocumentTypeDefinition (\ide _ TypeDefinitionParams{..} ->+ <> mkPluginHandler SMethod_TextDocumentTypeDefinition (\ide _ TypeDefinitionParams{..} -> gotoTypeDefinition ide TextDocumentPositionParams{..})- <> mkPluginHandler STextDocumentDocumentHighlight (\ide _ DocumentHighlightParams{..} ->+ <> mkPluginHandler SMethod_TextDocumentDocumentHighlight (\ide _ DocumentHighlightParams{..} -> documentHighlight ide TextDocumentPositionParams{..})- <> mkPluginHandler STextDocumentReferences (\ide _ params -> references ide params)- <> mkPluginHandler SWorkspaceSymbol (\ide _ params -> wsSymbols ide params),+ <> mkPluginHandler SMethod_TextDocumentReferences references+ <> mkPluginHandler SMethod_WorkspaceSymbol wsSymbols, pluginConfigDescriptor = defaultConfigDescriptor } -- --------------------------------------------------------------------- -hover' :: IdeState -> PluginId -> HoverParams -> LspM c (Either ResponseError (Maybe Hover))+hover' :: PluginMethodHandler IdeState 'Method_TextDocumentHover hover' ideState _ HoverParams{..} = do liftIO $ logDebug (ideLogger ideState) "GhcIde.hover entered (ideLogger)" -- AZ hover ideState TextDocumentPositionParams{..}---- ----------------------------------------------------------------------symbolsProvider :: IdeState -> PluginId -> DocumentSymbolParams -> LspM c (Either ResponseError (List DocumentSymbol |? List SymbolInformation))-symbolsProvider ide _ params = moduleOutline ide params---- ---------------------------------------------------------------------
src/Development/IDE/Plugin/Test.hs view
@@ -14,14 +14,18 @@ import Control.Concurrent (threadDelay) import Control.Monad+import Control.Monad.Except (ExceptT (..), throwError) import Control.Monad.IO.Class import Control.Monad.STM-import Data.Aeson-import Data.Aeson.Types+import Control.Monad.Trans.Class (MonadTrans (lift))+import Data.Aeson (FromJSON (parseJSON),+ ToJSON (toJSON), Value)+import qualified Data.Aeson.Types as A import Data.Bifunctor import Data.CaseInsensitive (CI, original) import qualified Data.HashMap.Strict as HM import Data.Maybe (isJust)+import Data.Proxy import Data.String import Data.Text (Text, pack) import Development.IDE.Core.OfInterest (getFilesOfInterest)@@ -43,9 +47,11 @@ import Development.IDE.Types.Location (fromUri) import GHC.Generics (Generic) import Ide.Plugin.Config (CheckParents)+import Ide.Plugin.Error import Ide.Types+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types import qualified Language.LSP.Server as LSP-import Language.LSP.Types import qualified "list-t" ListT import qualified StmContainers.Map as STM import System.Time.Extra@@ -73,26 +79,26 @@ plugin :: PluginDescriptor IdeState plugin = (defaultPluginDescriptor "test") {- pluginHandlers = mkPluginHandler (SCustomMethod "test") $ \st _ ->+ pluginHandlers = mkPluginHandler (SMethod_CustomMethod (Proxy @"test")) $ \st _ -> testRequestHandler' st } where testRequestHandler' ide req- | Just customReq <- parseMaybe parseJSON req- = testRequestHandler ide customReq+ | Just customReq <- A.parseMaybe parseJSON req+ = ExceptT $ testRequestHandler ide customReq | otherwise- = return $ Left- $ ResponseError InvalidRequest "Cannot parse request" Nothing+ = throwError+ $ PluginInvalidParams "Cannot parse request" testRequestHandler :: IdeState -> TestRequest- -> LSP.LspM c (Either ResponseError Value)+ -> LSP.LspM c (Either PluginError Value) testRequestHandler _ (BlockSeconds secs) = do- LSP.sendNotification (SCustomMethod "ghcide/blocking/request") $+ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/blocking/request")) $ toJSON secs liftIO $ sleep secs- return (Right Null)+ return (Right A.Null) testRequestHandler s (GetInterfaceFilesDir file) = liftIO $ do let nfp = fromUri $ toNormalizedUri file sess <- runAction "Test - GhcSession" s $ use_ GhcSession nfp@@ -105,12 +111,12 @@ atomically $ do n <- countQueue $ actionQueue $ shakeExtras s when (n>0) retry- return $ Right Null+ return $ Right A.Null testRequestHandler s (WaitForIdeRule k file) = liftIO $ do let nfp = fromUri $ toNormalizedUri file success <- runAction ("WaitForIdeRule " <> k <> " " <> show file) s $ parseAction (fromString k) nfp let res = WaitForIdeRuleResult <$> success- return $ bimap mkResponseError toJSON res+ return $ bimap PluginInvalidParams toJSON res testRequestHandler s GetBuildKeysBuilt = liftIO $ do keys <- getDatabaseKeys resultBuilt $ shakeDb s return $ Right $ toJSON $ map show keys@@ -144,9 +150,6 @@ step <- shakeGetBuildStep db return [ k | (k, res) <- keys, field res == Step step] -mkResponseError :: Text -> ResponseError-mkResponseError msg = ResponseError InvalidRequest msg Nothing- parseAction :: CI String -> NormalizedFilePath -> Action (Either Text Bool) parseAction "typecheck" fp = Right . isJust <$> use TypeCheck fp parseAction "getLocatedImports" fp = Right . isJust <$> use GetLocatedImports fp@@ -170,6 +173,6 @@ blockCommandHandler :: CommandFunction state ExecuteCommandParams blockCommandHandler _ideState _params = do- LSP.sendNotification (SCustomMethod "ghcide/blocking/command") Null+ lift $ LSP.sendNotification (SMethod_CustomMethod (Proxy @"ghcide/blocking/command")) A.Null liftIO $ threadDelay maxBound- return (Right Null)+ pure $ InR Null
src/Development/IDE/Plugin/TypeLenses.hs view
@@ -18,19 +18,20 @@ import Control.Monad (mzero) import Control.Monad.Extra (whenMaybe) import Control.Monad.IO.Class (MonadIO (liftIO))-import Data.Aeson.Types (Value (..), toJSON)+import Control.Monad.Trans.Class (MonadTrans (lift))+import Data.Aeson.Types (Value, toJSON) import qualified Data.Aeson.Types as A-import qualified Data.HashMap.Strict as Map import Data.List (find)+import qualified Data.Map as Map import Data.Maybe (catMaybes, mapMaybe) import qualified Data.Text as T import Development.IDE (GhcSession (..), HscEnvEq (hscEnv), RuleResult, Rules, define, srcSpanToRange,- usePropertyAction,- useWithStale)+ usePropertyAction) import Development.IDE.Core.Compile (TcModuleResult (..))+import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping (PositionMapping, toCurrentRange) import Development.IDE.Core.Rules (IdeState, runAction)@@ -46,12 +47,13 @@ import Development.IDE.Spans.LocalBindings (Bindings, getFuzzyScope) import Development.IDE.Types.Location (Position (Position, _character, _line), Range (Range, _end, _start))-import Development.IDE.Types.Logger (Pretty (pretty),+import GHC.Generics (Generic)+import Ide.Logger (Pretty (pretty), Recorder, WithPriority, cmapWithPrio)-import GHC.Generics (Generic)+import Ide.Plugin.Error import Ide.Plugin.Properties-import Ide.PluginUtils+import Ide.PluginUtils (mkLspCommand) import Ide.Types (CommandFunction, CommandId (CommandId), PluginCommand (PluginCommand),@@ -63,17 +65,18 @@ defaultPluginDescriptor, mkCustomConfig, mkPluginHandler)-import qualified Language.LSP.Server as LSP-import Language.LSP.Types (ApplyWorkspaceEditParams (ApplyWorkspaceEditParams),+import Language.LSP.Protocol.Message (Method (Method_TextDocumentCodeLens),+ SMethod (..))+import Language.LSP.Protocol.Types (ApplyWorkspaceEditParams (ApplyWorkspaceEditParams), CodeLens (CodeLens), CodeLensParams (CodeLensParams, _textDocument), Diagnostic (..),- List (..),- Method (TextDocumentCodeLens),- SMethod (..),+ Null (Null), TextDocumentIdentifier (TextDocumentIdentifier), TextEdit (TextEdit),- WorkspaceEdit (WorkspaceEdit))+ WorkspaceEdit (WorkspaceEdit),+ type (|?) (..))+import qualified Language.LSP.Server as LSP import Text.Regex.TDFA ((=~), (=~~)) data Log = LogShake Shake.Log deriving Show@@ -88,7 +91,7 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = (defaultPluginDescriptor plId)- { pluginHandlers = mkPluginHandler STextDocumentCodeLens codeLensProvider+ { pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeLens codeLensProvider , pluginCommands = [PluginCommand (CommandId typeLensCommandId) "adds a signature" commandHandler] , pluginRules = rules recorder , pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties}@@ -102,34 +105,28 @@ , (Diagnostics, "Follows error messages produced by GHC about missing signatures") ] Always -codeLensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens-codeLensProvider ideState pId CodeLensParams{_textDocument = TextDocumentIdentifier uri} = pluginResponse $ do+codeLensProvider :: PluginMethodHandler IdeState Method_TextDocumentCodeLens+codeLensProvider ideState pId CodeLensParams{_textDocument = TextDocumentIdentifier uri} = do mode <- liftIO $ runAction "codeLens.config" ideState $ usePropertyAction #mode pId properties- nfp <- getNormalizedFilePath uri- env <- hscEnv . fst- <$> (handleMaybeM "Unable to get GhcSession"- $ liftIO- $ runAction "codeLens.GhcSession" ideState (useWithStale GhcSession nfp)- )- tmr <- fst <$> (- handleMaybeM "Unable to TypeCheck"- $ liftIO- $ runAction "codeLens.TypeCheck" ideState (useWithStale TypeCheck nfp)- )- bindings <- fst <$> (- handleMaybeM "Unable to GetBindings"- $ liftIO- $ runAction "codeLens.GetBindings" ideState (useWithStale GetBindings nfp)- )+ nfp <- getNormalizedFilePathE uri+ env <- hscEnv . fst <$>+ runActionE "codeLens.GhcSession" ideState+ (useWithStaleE GhcSession nfp)++ (tmr, _) <- runActionE "codeLens.TypeCheck" ideState+ (useWithStaleE TypeCheck nfp)++ (bindings, _) <- runActionE "codeLens.GetBindings" ideState+ (useWithStaleE GetBindings nfp)+ (gblSigs@(GlobalBindingTypeSigsResult gblSigs'), gblSigsMp) <-- handleMaybeM "Unable to GetGlobalBindingTypeSigs"- $ liftIO- $ runAction "codeLens.GetGlobalBindingTypeSigs" ideState (useWithStale GetGlobalBindingTypeSigs nfp)+ runActionE "codeLens.GetGlobalBindingTypeSigs" ideState+ (useWithStaleE GetGlobalBindingTypeSigs nfp) diag <- liftIO $ atomically $ getDiagnostics ideState hDiag <- liftIO $ atomically $ getHiddenDiagnostics ideState - let toWorkSpaceEdit tedit = WorkspaceEdit (Just $ Map.singleton uri $ List tedit) Nothing Nothing+ let toWorkSpaceEdit tedit = WorkspaceEdit (Just $ Map.singleton uri $ tedit) Nothing Nothing generateLensForGlobal mp sig@GlobalBindingTypeSig{gbRendered} = do range <- toCurrentRange mp =<< srcSpanToRange (gbSrcSpan sig) tedit <- gblBindingTypeSigToEdit sig (Just gblSigsMp)@@ -144,7 +141,7 @@ ] -- `suggestLocalSignature` relies on diagnostic, if diagnostics don't have the local signature warning, -- the `bindings` is useless, and if diagnostic has, that means we parsed success, and the `bindings` is fresh.- pure $ List $ case mode of+ pure $ InL $ case mode of Always -> mapMaybe (generateLensForGlobal gblSigsMp) gblSigs' <> generateLensFromDiags@@ -160,8 +157,8 @@ commandHandler :: CommandFunction IdeState WorkspaceEdit commandHandler _ideState wedit = do- _ <- LSP.sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ())- return $ Right Null+ _ <- lift $ LSP.sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ())+ pure $ InR Null --------------------------------------------------------------------------------
src/Development/IDE/Spans/AtPoint.hs view
@@ -1,9 +1,10 @@ -- Copyright (c) 2019 The DAML Authors. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 -{-# LANGUAGE CPP #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-} -- | Gives information about symbols at a given point in DAML files. -- These are all pure functions that should execute quickly.@@ -27,7 +28,8 @@ import Development.IDE.GHC.Error import Development.IDE.GHC.Orphans () import Development.IDE.Types.Location-import Language.LSP.Types+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..)) -- compiler and infrastructure import Development.IDE.Core.PositionMapping@@ -178,8 +180,8 @@ DocumentHighlight (realSrcSpanToRange sp) (Just $ highlightType $ identInfo dets) highlightType s = if any (isJust . getScopeFromContext) s- then HkWrite- else HkRead+ then DocumentHighlightKind_Write+ else DocumentHighlightKind_Read gotoTypeDefinition :: MonadIO m@@ -212,21 +214,33 @@ -> DocAndKindMap -> HscEnv -> Position- -> Maybe (Maybe Range, [T.Text])-atPoint IdeOptions{} (HAR _ hf _ _ kind) (DKMap dm km) env pos = listToMaybe $ pointCommand hf pos hoverInfo+ -> IO (Maybe (Maybe Range, [T.Text]))+atPoint IdeOptions{} (HAR _ hf _ _ (kind :: HieKind hietype)) (DKMap dm km) env pos =+ listToMaybe <$> sequence (pointCommand hf pos hoverInfo) where -- Hover info for values/data- hoverInfo ast = (Just range, prettyNames ++ pTypes)+ hoverInfo :: HieAST hietype -> IO (Maybe Range, [T.Text])+ hoverInfo ast = do+ prettyNames <- mapM prettyName filteredNames+ pure (Just range, prettyNames ++ pTypes) where+ pTypes :: [T.Text] pTypes | Prelude.length names == 1 = dropEnd1 $ map wrapHaskell prettyTypes | otherwise = map wrapHaskell prettyTypes + range :: Range range = realSrcSpanToRange $ nodeSpan ast + wrapHaskell :: T.Text -> T.Text wrapHaskell x = "\n```haskell\n"<>x<>"\n```\n"++ info :: NodeInfo hietype info = nodeInfoH kind ast++ names :: [(Identifier, IdentifierDetails hietype)] names = M.assocs $ nodeIdentifiers info+ -- Check for evidence bindings isInternal :: (Identifier, IdentifierDetails a) -> Bool isInternal (Right _, dets) =@@ -236,11 +250,12 @@ False #endif isInternal (Left _, _) = False++ filteredNames :: [(Identifier, IdentifierDetails hietype)] filteredNames = filter (not . isInternal) names- types = nodeType info- prettyNames :: [T.Text]- prettyNames = map prettyName filteredNames- prettyName (Right n, dets) = T.unlines $++ prettyName :: (Either ModuleName Name, IdentifierDetails hietype) -> IO T.Text+ prettyName (Right n, dets) = pure $ T.unlines $ wrapHaskell (printOutputable n <> maybe "" (" :: " <>) ((prettyType <$> identType dets) <|> maybeKind)) : maybeToList (pretty (definedAt n) (prettyPackageName n)) ++ catMaybes [ T.unlines . spanDocToMarkdown <$> lookupNameEnv dm n@@ -250,21 +265,48 @@ pretty (Just define) Nothing = Just $ define <> "\n" pretty Nothing (Just pkgName) = Just $ pkgName <> "\n" pretty (Just define) (Just pkgName) = Just $ define <> " " <> pkgName <> "\n"- prettyName (Left m,_) = printOutputable m+ prettyName (Left m,_) = packageNameForImportStatement m + prettyPackageName :: Name -> Maybe T.Text prettyPackageName n = do m <- nameModule_maybe n+ pkgTxt <- packageNameWithVersion m env+ pure $ "*(" <> pkgTxt <> ")*"++ -- Return the module text itself and+ -- the package(with version) this `ModuleName` belongs to.+ packageNameForImportStatement :: ModuleName -> IO T.Text+ packageNameForImportStatement mod = do+ mpkg <- findImportedModule env mod :: IO (Maybe Module)+ let moduleName = printOutputable mod+ case mpkg >>= flip packageNameWithVersion env of+ Nothing -> pure moduleName+ Just pkgWithVersion -> pure $ moduleName <> "\n\n" <> pkgWithVersion++ -- Return the package name and version of a module.+ -- For example, given module `Data.List`, it should return something like `base-4.x`.+ packageNameWithVersion :: Module -> HscEnv -> Maybe T.Text+ packageNameWithVersion m env = do let pid = moduleUnit m conf <- lookupUnit env pid let pkgName = T.pack $ unitPackageNameString conf version = T.pack $ showVersion (unitPackageVersion conf)- pure $ "*(" <> pkgName <> "-" <> version <> ")*"+ pure $ pkgName <> "-" <> version + -- Type info for the current node, it may contains several symbols+ -- for one range, like wildcard+ types :: [hietype]+ types = nodeType info++ prettyTypes :: [T.Text] prettyTypes = map (("_ :: "<>) . prettyType) types++ prettyType :: hietype -> T.Text prettyType t = case kind of HieFresh -> printOutputable t HieFromDisk full_file -> printOutputable $ hieTypeToIface $ recoverFullType t (hie_types full_file) + definedAt :: Name -> Maybe T.Text definedAt name = -- do not show "at <no location info>" and similar messages -- see the code of 'pprNameDefnLoc' for more information@@ -391,13 +433,15 @@ defRowToSymbolInfo :: Res DefRow -> Maybe SymbolInformation defRowToSymbolInfo (DefRow{..}:.(modInfoSrcFile -> Just srcFile))- = Just $ SymbolInformation (printOutputable defNameOcc) kind Nothing Nothing loc Nothing+ = Just $ SymbolInformation (printOutputable defNameOcc) kind Nothing Nothing Nothing loc where kind- | isVarOcc defNameOcc = SkVariable- | isDataOcc defNameOcc = SkConstructor- | isTcOcc defNameOcc = SkStruct- | otherwise = SkUnknown 1+ | isVarOcc defNameOcc = SymbolKind_Variable+ | isDataOcc defNameOcc = SymbolKind_Constructor+ | isTcOcc defNameOcc = SymbolKind_Struct+ -- This used to be (SkUnknown 1), buth there is no SymbolKind_Unknown.+ -- Changing this to File, as that is enum representation of 1+ | otherwise = SymbolKind_File loc = Location file range file = fromNormalizedUri . filePathToUri' . toNormalizedFilePath' $ srcFile range = Range start end@@ -424,6 +468,7 @@ where sloc fs = mkRealSrcLoc fs (fromIntegral $ line+1) (fromIntegral $ cha+1) sp fs = mkRealSrcSpan (sloc fs) (sloc fs)+ line :: UInt line = _line pos cha = _character pos
src/Development/IDE/Spans/Documentation.hs view
@@ -32,7 +32,7 @@ import System.Directory import System.FilePath -import Language.LSP.Types (filePathToUri, getUri)+import Language.LSP.Protocol.Types (filePathToUri, getUri) #if MIN_VERSION_ghc(9,3,0) import GHC.Types.Unique.Map #endif
src/Development/IDE/Spans/Pragmas.hs view
@@ -15,15 +15,16 @@ import qualified Data.Maybe as Maybe import Data.Text (Text, pack) import qualified Data.Text as Text-import Development.IDE (srcSpanToRange, IdeState, NormalizedFilePath, runAction, useWithStale, GhcSession (..), getFileContents, hscEnv)+import Development.IDE (srcSpanToRange, IdeState, NormalizedFilePath, GhcSession (..), getFileContents, hscEnv, runAction) import Development.IDE.GHC.Compat import Development.IDE.GHC.Compat.Util-import qualified Language.LSP.Types as LSP+import qualified Language.LSP.Protocol.Types as LSP import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.Trans.Except (ExceptT)+import Ide.Plugin.Error (PluginError) import Ide.Types (PluginId(..)) import qualified Data.Text as T-import Ide.PluginUtils (handleMaybeM)+import Development.IDE.Core.PluginUtils getNextPragmaInfo :: DynFlags -> Maybe Text -> NextPragmaInfo getNextPragmaInfo dynFlags sourceText =@@ -51,13 +52,11 @@ pragmaInsertPosition = LSP.Position (fromIntegral nextPragmaLine) 0 pragmaInsertRange = LSP.Range pragmaInsertPosition pragmaInsertPosition -getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT String m NextPragmaInfo-getFirstPragma (PluginId pId) state nfp = handleMaybeM "Could not get NextPragmaInfo" $ do- ghcSession <- liftIO $ runAction (T.unpack pId <> ".GhcSession") state $ useWithStale GhcSession nfp+getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError m NextPragmaInfo+getFirstPragma (PluginId pId) state nfp = do+ (hscEnv -> hsc_dflags -> sessionDynFlags, _) <- runActionE (T.unpack pId <> ".GhcSession") state $ useWithStaleE GhcSession nfp (_, fileContents) <- liftIO $ runAction (T.unpack pId <> ".GetFileContents") state $ getFileContents nfp- case ghcSession of- Just (hscEnv -> hsc_dflags -> sessionDynFlags, _) -> pure $ Just $ getNextPragmaInfo sessionDynFlags fileContents- Nothing -> pure Nothing+ pure $ getNextPragmaInfo sessionDynFlags fileContents -- Pre-declaration comments parser -----------------------------------------------------
src/Development/IDE/Types/Action.hs view
@@ -11,12 +11,12 @@ where import Control.Concurrent.STM-import Data.Hashable (Hashable (..))-import Data.HashSet (HashSet)-import qualified Data.HashSet as Set-import Data.Unique (Unique)-import Development.IDE.Graph (Action)-import Development.IDE.Types.Logger+import Data.Hashable (Hashable (..))+import Data.HashSet (HashSet)+import qualified Data.HashSet as Set+import Data.Unique (Unique)+import Development.IDE.Graph (Action)+import Ide.Logger import Numeric.Natural data DelayedAction a = DelayedAction
src/Development/IDE/Types/Diagnostics.hs view
@@ -9,7 +9,6 @@ IdeResult, LSP.DiagnosticSeverity(..), DiagnosticStore,- List(..), ideErrorText, ideErrorWithSource, showDiagnostics,@@ -17,20 +16,17 @@ IdeResultNoDiagnosticsEarlyCutoff) where import Control.DeepSeq-import Data.Maybe as Maybe-import qualified Data.Text as T-import Data.Text.Prettyprint.Doc-import Data.Text.Prettyprint.Doc.Render.Terminal (Color (..), color)-import qualified Data.Text.Prettyprint.Doc.Render.Terminal as Terminal-import Data.Text.Prettyprint.Doc.Render.Text-import Language.LSP.Diagnostics-import Language.LSP.Types as LSP (Diagnostic (..),- DiagnosticSeverity (..),- DiagnosticSource,- List (..))--import Data.ByteString (ByteString)+import Data.ByteString (ByteString)+import Data.Maybe as Maybe+import qualified Data.Text as T import Development.IDE.Types.Location+import Language.LSP.Diagnostics+import Language.LSP.Protocol.Types as LSP (Diagnostic (..),+ DiagnosticSeverity (..))+import Prettyprinter+import Prettyprinter.Render.Terminal (Color (..), color)+import qualified Prettyprinter.Render.Terminal as Terminal+import Prettyprinter.Render.Text -- | The result of an IDE operation. Warnings and errors are in the Diagnostic,@@ -49,10 +45,10 @@ type IdeResultNoDiagnosticsEarlyCutoff v = (Maybe ByteString, Maybe v) ideErrorText :: NormalizedFilePath -> T.Text -> FileDiagnostic-ideErrorText = ideErrorWithSource (Just "compiler") (Just DsError)+ideErrorText = ideErrorWithSource (Just "compiler") (Just DiagnosticSeverity_Error) ideErrorWithSource- :: Maybe DiagnosticSource+ :: Maybe T.Text -> Maybe DiagnosticSeverity -> a -> T.Text@@ -64,7 +60,9 @@ _source = source, _message = msg, _relatedInformation = Nothing,- _tags = Nothing+ _tags = Nothing,+ _codeDescription = Nothing,+ _data_ = Nothing }) -- | Defines whether a particular diagnostic should be reported@@ -117,14 +115,14 @@ , slabel_ "Severity:" $ pretty $ show sev , slabel_ "Message: " $ case sev of- LSP.DsError -> annotate $ color Red- LSP.DsWarning -> annotate $ color Yellow- LSP.DsInfo -> annotate $ color Blue- LSP.DsHint -> annotate $ color Magenta+ LSP.DiagnosticSeverity_Error -> annotate $ color Red+ LSP.DiagnosticSeverity_Warning -> annotate $ color Yellow+ LSP.DiagnosticSeverity_Information -> annotate $ color Blue+ LSP.DiagnosticSeverity_Hint -> annotate $ color Magenta $ stringParagraphs _message ] where- sev = fromMaybe LSP.DsError _severity+ sev = fromMaybe LSP.DiagnosticSeverity_Error _severity -- | Label a document.
src/Development/IDE/Types/HscEnvEq.hs view
@@ -4,6 +4,7 @@ hscEnvWithImportPaths, newHscEnvEqPreserveImportPaths, newHscEnvEqWithImportPaths,+ updateHscEnvEq, envImportPaths, envPackageExports, envVisibleModuleNames,@@ -32,7 +33,8 @@ import System.FilePath -- | An 'HscEnv' with equality. Two values are considered equal--- if they are created with the same call to 'newHscEnvEq'.+-- if they are created with the same call to 'newHscEnvEq' or+-- 'updateHscEnvEq'. data HscEnvEq = HscEnvEq { envUnique :: !Unique , hscEnv :: !HscEnv@@ -50,6 +52,11 @@ -- So it's wrapped in IO here for error handling -- If Nothing, 'listVisibleModuleNames' panic }++updateHscEnvEq :: HscEnvEq -> HscEnv -> IO HscEnvEq+updateHscEnvEq oldHscEnvEq newHscEnv = do+ let update newUnique = oldHscEnvEq { envUnique = newUnique, hscEnv = newHscEnv }+ update <$> Unique.newUnique -- | Wrap an 'HscEnv' into an 'HscEnvEq'. newHscEnvEq :: FilePath -> HscEnv -> [(UnitId, DynFlags)] -> IO HscEnvEq
src/Development/IDE/Types/Location.hs view
@@ -39,9 +39,9 @@ import FastString import SrcLoc as GHC #endif-import Language.LSP.Types (Location (..), Position (..),+import Language.LSP.Protocol.Types (Location (..), Position (..), Range (..))-import qualified Language.LSP.Types as LSP+import qualified Language.LSP.Protocol.Types as LSP import Text.ParserCombinators.ReadP as ReadP toNormalizedFilePath' :: FilePath -> LSP.NormalizedFilePath
− src/Development/IDE/Types/Logger.hs
@@ -1,335 +0,0 @@--- Copyright (c) 2019 The DAML Authors. All rights reserved.--- SPDX-License-Identifier: Apache-2.0--{-# LANGUAGE CPP #-}-{-# LANGUAGE RankNTypes #-}--- | This is a compatibility module that abstracts over the--- concrete choice of logging framework so users can plug in whatever--- framework they want to.-module Development.IDE.Types.Logger- ( Priority(..)- , Logger(..)- , Recorder(..)- , logError, logWarning, logInfo, logDebug- , noLogging- , WithPriority(..)- , logWith- , cmap- , cmapIO- , cfilter- , withDefaultRecorder- , makeDefaultStderrRecorder- , makeDefaultHandleRecorder- , LoggingColumn(..)- , cmapWithPrio- , withBacklog- , lspClientMessageRecorder- , lspClientLogRecorder- , module PrettyPrinterModule- , renderStrict- , toCologActionWithPrio- ) where--import Control.Concurrent (myThreadId)-import Control.Concurrent.Extra (Lock, newLock, withLock)-import Control.Concurrent.STM (atomically,- flushTBQueue,- isFullTBQueue,- newTBQueueIO, newTVarIO,- readTVarIO,- writeTBQueue, writeTVar)-import Control.Exception (IOException)-import Control.Monad (unless, when, (>=>))-import Control.Monad.IO.Class (MonadIO (liftIO))-import Data.Foldable (for_)-import Data.Functor.Contravariant (Contravariant (contramap))-import Data.Maybe (fromMaybe)-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text as Text-import qualified Data.Text.IO as Text-import Data.Time (defaultTimeLocale,- formatTime,- getCurrentTime)-import GHC.Stack (CallStack, HasCallStack,- SrcLoc (SrcLoc, srcLocModule, srcLocStartCol, srcLocStartLine),- callStack, getCallStack,- withFrozenCallStack)-import Language.LSP.Server-import qualified Language.LSP.Server as LSP-import Language.LSP.Types (LogMessageParams (..),- MessageType (..),- SMethod (SWindowLogMessage, SWindowShowMessage),- ShowMessageParams (..))-#if MIN_VERSION_prettyprinter(1,7,0)-import Prettyprinter as PrettyPrinterModule-import Prettyprinter.Render.Text (renderStrict)-#else-import Data.Text.Prettyprint.Doc as PrettyPrinterModule-import Data.Text.Prettyprint.Doc.Render.Text (renderStrict)-#endif-import Colog.Core (LogAction (..),- Severity,- WithSeverity (..))-import qualified Colog.Core as Colog-import System.IO (Handle,- IOMode (AppendMode),- hClose, hFlush,- openFile, stderr)-import UnliftIO (MonadUnliftIO,- displayException,- finally, try)--data Priority--- Don't change the ordering of this type or you will mess up the Ord--- instance- = Debug -- ^ Verbose debug logging.- | Info -- ^ Useful information in case an error has to be understood.- | Warning- -- ^ These error messages should not occur in a expected usage, and- -- should be investigated.- | Error -- ^ Such log messages must never occur in expected usage.- deriving (Eq, Show, Ord, Enum, Bounded)---- | Note that this is logging actions _of the program_, not of the user.--- You shouldn't call warning/error if the user has caused an error, only--- if our code has gone wrong and is itself erroneous (e.g. we threw an exception).-newtype Logger = Logger {logPriority :: Priority -> T.Text -> IO ()}--instance Semigroup Logger where- l1 <> l2 = Logger $ \p t -> logPriority l1 p t >> logPriority l2 p t--instance Monoid Logger where- mempty = Logger $ \_ _ -> pure ()--logError :: Logger -> T.Text -> IO ()-logError x = logPriority x Error--logWarning :: Logger -> T.Text -> IO ()-logWarning x = logPriority x Warning--logInfo :: Logger -> T.Text -> IO ()-logInfo x = logPriority x Info--logDebug :: Logger -> T.Text -> IO ()-logDebug x = logPriority x Debug--noLogging :: Logger-noLogging = Logger $ \_ _ -> return ()--data WithPriority a = WithPriority { priority :: Priority, callStack_ :: CallStack, payload :: a } deriving Functor---- | Note that this is logging actions _of the program_, not of the user.--- You shouldn't call warning/error if the user has caused an error, only--- if our code has gone wrong and is itself erroneous (e.g. we threw an exception).-newtype Recorder msg = Recorder- { logger_ :: forall m. (MonadIO m) => msg -> m () }--logWith :: (HasCallStack, MonadIO m) => Recorder (WithPriority msg) -> Priority -> msg -> m ()-logWith recorder priority msg = withFrozenCallStack $ logger_ recorder (WithPriority priority callStack msg)--instance Semigroup (Recorder msg) where- (<>) Recorder{ logger_ = logger_1 } Recorder{ logger_ = logger_2 } =- Recorder- { logger_ = \msg -> logger_1 msg >> logger_2 msg }--instance Monoid (Recorder msg) where- mempty =- Recorder- { logger_ = \_ -> pure () }--instance Contravariant Recorder where- contramap f Recorder{ logger_ } =- Recorder- { logger_ = logger_ . f }--cmap :: (a -> b) -> Recorder b -> Recorder a-cmap = contramap--cmapWithPrio :: (a -> b) -> Recorder (WithPriority b) -> Recorder (WithPriority a)-cmapWithPrio f = cmap (fmap f)--cmapIO :: (a -> IO b) -> Recorder b -> Recorder a-cmapIO f Recorder{ logger_ } =- Recorder- { logger_ = (liftIO . f) >=> logger_ }--cfilter :: (a -> Bool) -> Recorder a -> Recorder a-cfilter p Recorder{ logger_ } =- Recorder- { logger_ = \msg -> when (p msg) (logger_ msg) }--textHandleRecorder :: Handle -> Recorder Text-textHandleRecorder handle =- Recorder- { logger_ = \text -> liftIO $ Text.hPutStrLn handle text *> hFlush handle }--makeDefaultStderrRecorder :: MonadIO m => Maybe [LoggingColumn] -> m (Recorder (WithPriority (Doc a)))-makeDefaultStderrRecorder columns = do- lock <- liftIO newLock- makeDefaultHandleRecorder columns lock stderr---- | If no path given then use stderr, otherwise use file.-withDefaultRecorder- :: MonadUnliftIO m- => Maybe FilePath- -- ^ Log file path. `Nothing` uses stderr- -> Maybe [LoggingColumn]- -- ^ logging columns to display. `Nothing` uses `defaultLoggingColumns`- -> (Recorder (WithPriority (Doc d)) -> m a)- -- ^ action given a recorder- -> m a-withDefaultRecorder path columns action = do- lock <- liftIO newLock- let makeHandleRecorder = makeDefaultHandleRecorder columns lock- case path of- Nothing -> do- recorder <- makeHandleRecorder stderr- let message = "No log file specified; using stderr."- logWith recorder Info message- action recorder- Just path -> do- fileHandle :: Either IOException Handle <- liftIO $ try (openFile path AppendMode)- case fileHandle of- Left e -> do- recorder <- makeHandleRecorder stderr- let exceptionMessage = pretty $ displayException e- let message = vcat [exceptionMessage, "Couldn't open log file" <+> pretty path <> "; falling back to stderr."]- logWith recorder Warning message- action recorder- Right fileHandle -> finally (makeHandleRecorder fileHandle >>= action) (liftIO $ hClose fileHandle)--makeDefaultHandleRecorder- :: MonadIO m- => Maybe [LoggingColumn]- -- ^ built-in logging columns to display. Nothing uses the default- -> Lock- -- ^ lock to take when outputting to handle- -> Handle- -- ^ handle to output to- -> m (Recorder (WithPriority (Doc a)))-makeDefaultHandleRecorder columns lock handle = do- let Recorder{ logger_ } = textHandleRecorder handle- let threadSafeRecorder = Recorder { logger_ = \msg -> liftIO $ withLock lock (logger_ msg) }- let loggingColumns = fromMaybe defaultLoggingColumns columns- let textWithPriorityRecorder = cmapIO (textWithPriorityToText loggingColumns) threadSafeRecorder- pure (cmap docToText textWithPriorityRecorder)- where- docToText = fmap (renderStrict . layoutPretty defaultLayoutOptions)--data LoggingColumn- = TimeColumn- | ThreadIdColumn- | PriorityColumn- | DataColumn- | SourceLocColumn--defaultLoggingColumns :: [LoggingColumn]-defaultLoggingColumns = [TimeColumn, PriorityColumn, DataColumn]--textWithPriorityToText :: [LoggingColumn] -> WithPriority Text -> IO Text-textWithPriorityToText columns WithPriority{ priority, callStack_, payload } = do- textColumns <- mapM loggingColumnToText columns- pure $ Text.intercalate " | " textColumns- where- showAsText :: Show a => a -> Text- showAsText = Text.pack . show-- utcTimeToText utcTime = Text.pack $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%6QZ" utcTime-- priorityToText :: Priority -> Text- priorityToText = showAsText-- threadIdToText = showAsText-- callStackToSrcLoc :: CallStack -> Maybe SrcLoc- callStackToSrcLoc callStack =- case getCallStack callStack of- (_, srcLoc) : _ -> Just srcLoc- _ -> Nothing-- srcLocToText = \case- Nothing -> "<unknown>"- Just SrcLoc{ srcLocModule, srcLocStartLine, srcLocStartCol } ->- Text.pack srcLocModule <> "#" <> showAsText srcLocStartLine <> ":" <> showAsText srcLocStartCol-- loggingColumnToText :: LoggingColumn -> IO Text- loggingColumnToText = \case- TimeColumn -> do- utcTime <- getCurrentTime- pure (utcTimeToText utcTime)- SourceLocColumn -> pure $ (srcLocToText . callStackToSrcLoc) callStack_- ThreadIdColumn -> do- threadId <- myThreadId- pure (threadIdToText threadId)- PriorityColumn -> pure (priorityToText priority)- DataColumn -> pure payload---- | Given a 'Recorder' that requires an argument, produces a 'Recorder'--- that queues up messages until the argument is provided using the callback, at which--- point it sends the backlog and begins functioning normally.-withBacklog :: (v -> Recorder a) -> IO (Recorder a, v -> IO ())-withBacklog recFun = do- -- Arbitrary backlog capacity- backlog <- newTBQueueIO 100- let backlogRecorder = Recorder $ \it -> liftIO $ atomically $ do- -- If the queue is full just drop the message on the floor. This is most likely- -- to happen if the callback is just never going to be called; in which case- -- we want neither to build up an unbounded backlog in memory, nor block waiting- -- for space!- full <- isFullTBQueue backlog- unless full $ writeTBQueue backlog it-- -- The variable holding the recorder starts out holding the recorder that writes- -- to the backlog.- recVar <- newTVarIO backlogRecorder- -- The callback atomically swaps out the recorder for the final one, and flushes- -- the backlog to it.- let cb arg = do- let recorder = recFun arg- toRecord <- atomically $ writeTVar recVar recorder >> flushTBQueue backlog- for_ toRecord (logger_ recorder)-- -- The recorder we actually return looks in the variable and uses whatever is there.- let varRecorder = Recorder $ \it -> do- r <- liftIO $ readTVarIO recVar- logger_ r it-- pure (varRecorder, cb)---- | Creates a recorder that sends logs to the LSP client via @window/showMessage@ notifications.-lspClientMessageRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)-lspClientMessageRecorder env = Recorder $ \WithPriority {..} ->- liftIO $ LSP.runLspT env $ LSP.sendNotification SWindowShowMessage- ShowMessageParams- { _xtype = priorityToLsp priority,- _message = payload- }---- | Creates a recorder that sends logs to the LSP client via @window/logMessage@ notifications.-lspClientLogRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)-lspClientLogRecorder env = Recorder $ \WithPriority {..} ->- liftIO $ LSP.runLspT env $ LSP.sendNotification SWindowLogMessage- LogMessageParams- { _xtype = priorityToLsp priority,- _message = payload- }--priorityToLsp :: Priority -> MessageType-priorityToLsp =- \case- Debug -> MtLog- Info -> MtInfo- Warning -> MtWarning- Error -> MtError--toCologActionWithPrio :: (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)-toCologActionWithPrio (Recorder _logger) = LogAction $ \WithSeverity{..} -> do- let priority = severityToPriority getSeverity- _logger $ WithPriority priority callStack getMsg- where- severityToPriority :: Severity -> Priority- severityToPriority Colog.Debug = Debug- severityToPriority Colog.Info = Info- severityToPriority Colog.Warning = Warning- severityToPriority Colog.Error = Error
src/Development/IDE/Types/Options.hs view
@@ -2,7 +2,8 @@ -- SPDX-License-Identifier: Apache-2.0 -- | Options-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE RankNTypes #-} module Development.IDE.Types.Options ( IdeOptions(..) , IdePreprocessedSource(..)@@ -18,7 +19,7 @@ , OptHaddockParse(..) , ProgressReportingStyle(..) ) where-+import Control.Lens import qualified Data.Text as T import Data.Typeable import Development.IDE.Core.RuleTypes@@ -27,8 +28,8 @@ import Development.IDE.Types.Diagnostics import Ide.Plugin.Config import Ide.Types (DynFlagsModifications)-import qualified Language.LSP.Types.Capabilities as LSP-+import qualified Language.LSP.Protocol.Lens as L+import qualified Language.LSP.Protocol.Types as LSP data IdeOptions = IdeOptions { optPreprocessor :: GHC.ParsedSource -> IdePreprocessedSource -- ^ Preprocessor to run over all parsed source trees, generating a list of warnings@@ -110,7 +111,7 @@ clientSupportsProgress :: LSP.ClientCapabilities -> IdeReportProgress clientSupportsProgress caps = IdeReportProgress $ Just True ==- (LSP._workDoneProgress =<< LSP._window (caps :: LSP.ClientCapabilities))+ ((\x -> x ^. L.workDoneProgress) =<< LSP._window (caps :: LSP.ClientCapabilities)) defaultIdeOptions :: Action IdeGhcSession -> IdeOptions defaultIdeOptions session = IdeOptions
src/Text/Fuzzy/Parallel.hs view
@@ -1,9 +1,9 @@ -- | Parallel versions of 'filter' and 'simpleFilter' module Text.Fuzzy.Parallel-( filter,- simpleFilter,- match,+( filter, filter',+ simpleFilter, simpleFilter',+ match, defChunkSize, defMaxResults, Scored(..) ) where @@ -29,7 +29,6 @@ -- Just 5 -- {-# INLINABLE match #-}- match :: T.Text -- ^ Pattern in lowercase except for first character -> T.Text -- ^ The text to search in. -> Maybe Int -- ^ The score@@ -70,23 +69,14 @@ toLowerAscii w = if (w - 65) < 26 then w .|. 0x20 else w --- | The function to filter a list of values by fuzzy search on the text extracted from them.-filter :: Int -- ^ Chunk size. 1000 works well.- -> Int -- ^ Max. number of results wanted- -> T.Text -- ^ Pattern.- -> [t] -- ^ The list of values containing the text to search in.- -> (t -> T.Text) -- ^ The function to extract the text from the container.- -> [Scored t] -- ^ The list of results, sorted, highest score first.-filter chunkSize maxRes pattern ts extract = partialSortByAscScore maxRes perfectScore (concat vss)- where- -- Preserve case for the first character, make all others lowercase- pattern' = case T.uncons pattern of- Just (c, rest) -> T.cons c (T.toLower rest)- _ -> pattern- vss = map (mapMaybe (\t -> flip Scored t <$> match pattern' (extract t))) (chunkList chunkSize ts)- `using` parList (evalList rseq)- perfectScore = fromMaybe (error $ T.unpack pattern) $ match pattern' pattern'+-- | Sensible default value for chunk size to use when calling simple filter.+defChunkSize :: Int+defChunkSize = 1000 +-- | Sensible default value for the number of max results to use when calling simple filter.+defMaxResults :: Int+defMaxResults = 10+ -- | Return all elements of the list that have a fuzzy -- match against the pattern. Runs with default settings where -- nothing is added around the matches, as case insensitive.@@ -102,6 +92,52 @@ simpleFilter chunk maxRes pattern xs = filter chunk maxRes pattern xs id ++-- | The function to filter a list of values by fuzzy search on the text extracted from them,+-- using a custom matching function which determines how close words are.+filter' :: Int -- ^ Chunk size. 1000 works well.+ -> Int -- ^ Max. number of results wanted+ -> T.Text -- ^ Pattern.+ -> [t] -- ^ The list of values containing the text to search in.+ -> (t -> T.Text) -- ^ The function to extract the text from the container.+ -> (T.Text -> T.Text -> Maybe Int)+ -- ^ Custom scoring function to use for calculating how close words are+ -- When the function returns Nothing, this means the values are incomparable.+ -> [Scored t] -- ^ The list of results, sorted, highest score first.+filter' chunkSize maxRes pattern ts extract match' = partialSortByAscScore maxRes perfectScore (concat vss)+ where+ -- Preserve case for the first character, make all others lowercase+ pattern' = case T.uncons pattern of+ Just (c, rest) -> T.cons c (T.toLower rest)+ _ -> pattern+ vss = map (mapMaybe (\t -> flip Scored t <$> match' pattern' (extract t))) (chunkList chunkSize ts)+ `using` parList (evalList rseq)+ perfectScore = fromMaybe (error $ T.unpack pattern) $ match' pattern' pattern'++-- | The function to filter a list of values by fuzzy search on the text extracted from them,+-- using a custom matching function which determines how close words are.+filter :: Int -- ^ Chunk size. 1000 works well.+ -> Int -- ^ Max. number of results wanted+ -> T.Text -- ^ Pattern.+ -> [t] -- ^ The list of values containing the text to search in.+ -> (t -> T.Text) -- ^ The function to extract the text from the container.+ -> [Scored t] -- ^ The list of results, sorted, highest score first.+filter chunkSize maxRes pattern ts extract =+ filter' chunkSize maxRes pattern ts extract match++-- | Return all elements of the list that have a fuzzy match against the pattern,+-- the closeness of the match is determined using the custom scoring match function that is passed.+-- Runs with default settings where nothing is added around the matches, as case insensitive.+{-# INLINABLE simpleFilter' #-}+simpleFilter' :: Int -- ^ Chunk size. 1000 works well.+ -> Int -- ^ Max. number of results wanted+ -> T.Text -- ^ Pattern to look for.+ -> [T.Text] -- ^ List of texts to check.+ -> (T.Text -> T.Text -> Maybe Int)+ -- ^ Custom scoring function to use for calculating how close words are+ -> [Scored T.Text] -- ^ The ones that match.+simpleFilter' chunk maxRes pattern xs match' =+ filter' chunk maxRes pattern xs id match' -------------------------------------------------------------------------------- chunkList :: Int -> [a] -> [[a]]
+ test/exe/AsyncTests.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DataKinds #-}++module AsyncTests (tests) where++import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.Aeson (toJSON)+import Data.Proxy+import qualified Data.Text as T+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+-- import Test.QuickCheck.Instances ()+import Development.IDE.Plugin.Test (TestRequest (BlockSeconds),+ blockCommandId)+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++-- | Test if ghcide asynchronously handles Commands and user Requests+tests :: TestTree+tests = testGroup "async"+ [+ testSession "command" $ do+ -- Execute a command that will block forever+ let req = ExecuteCommandParams Nothing blockCommandId Nothing+ void $ sendRequest SMethod_WorkspaceExecuteCommand req+ -- Load a file and check for code actions. Will only work if the command is run asynchronously+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS -Wmissing-signatures #-}"+ , "foo = id"+ ]+ void waitForDiagnostics+ codeLenses <- getCodeLenses doc+ liftIO $ [ _title | CodeLens{_command = Just Command{_title}} <- codeLenses] @=?+ [ "foo :: a -> a" ]+ , testSession "request" $ do+ -- Execute a custom request that will block for 1000 seconds+ void $ sendRequest (SMethod_CustomMethod (Proxy @"test")) $ toJSON $ BlockSeconds 1000+ -- Load a file and check for code actions. Will only work if the request is run asynchronously+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS -Wmissing-signatures #-}"+ , "foo = id"+ ]+ void waitForDiagnostics+ codeLenses <- getCodeLenses doc+ liftIO $ [ _title | CodeLens{_command = Just Command{_title}} <- codeLenses] @=?+ [ "foo :: a -> a" ]+ ]
+ test/exe/BootTests.hs view
@@ -0,0 +1,55 @@+module BootTests (tests) where++import Control.Applicative.Combinators+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Development.IDE.GHC.Util+import Development.IDE.Test (expectNoMoreDiagnostics,+ isReferenceReady)+import Development.IDE.Types.Location+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.FilePath+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+++tests :: TestTree+tests = testGroup "boot"+ [ testCase "boot-def-test" $ runWithExtraFiles "boot" $ \dir -> do+ let cPath = dir </> "C.hs"+ cSource <- liftIO $ readFileUtf8 cPath+ -- Dirty the cache+ liftIO $ runInDir dir $ do+ cDoc <- createDoc cPath "haskell" cSource+ -- We send a hover request then wait for either the hover response or+ -- `ghcide/reference/ready` notification.+ -- Once we receive one of the above, we wait for the other that we+ -- haven't received yet.+ -- If we don't wait for the `ready` notification it is possible+ -- that the `getDefinitions` request/response in the outer ghcide+ -- session will find no definitions.+ let hoverParams = HoverParams cDoc (Position 4 3) Nothing+ hoverRequestId <- sendRequest SMethod_TextDocumentHover hoverParams+ let parseReadyMessage = isReferenceReady cPath+ let parseHoverResponse = responseForId SMethod_TextDocumentHover hoverRequestId+ hoverResponseOrReadyMessage <- skipManyTill anyMessage ((Left <$> parseHoverResponse) <|> (Right <$> parseReadyMessage))+ _ <- skipManyTill anyMessage $+ case hoverResponseOrReadyMessage of+ Left _ -> void parseReadyMessage+ Right _ -> void parseHoverResponse+ closeDoc cDoc+ cdoc <- createDoc cPath "haskell" cSource+ locs <- getDefinitions cdoc (Position 7 4)+ let floc = mkR 9 0 9 1+ checkDefs locs (pure [floc])+ , testCase "graph with boot modules" $ runWithExtraFiles "boot2" $ \dir -> do+ _ <- openDoc (dir </> "A.hs") "haskell"+ expectNoMoreDiagnostics 2+ ]
+ test/exe/CPPTests.hs view
@@ -0,0 +1,56 @@+module CPPTests (tests) where++import Control.Exception (catch)+import qualified Data.Text as T+import Development.IDE.Test (Cursor, expectDiagnostics,+ expectNoMoreDiagnostics)+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests =+ testGroup "cpp"+ [ ignoreInWindowsBecause "Throw a lsp session time out in windows for ghc-8.8 and is broken for other versions" $ testCase "cpp-error" $ do+ let content =+ T.unlines+ [ "{-# LANGUAGE CPP #-}",+ "module Testing where",+ "#ifdef FOO",+ "foo = 42"+ ]+ -- The error locations differ depending on which C-preprocessor is used.+ -- Some give the column number and others don't (hence maxBound == -1 unsigned). Assert either+ -- of them.+ (run $ expectError content (2, maxBound))+ `catch` ( \e -> do+ let _ = e :: HUnitFailure+ run $ expectError content (2, 1)+ )+ , testSessionWait "cpp-ghcide" $ do+ _ <- createDoc "A.hs" "haskell" $ T.unlines+ ["{-# LANGUAGE CPP #-}"+ ,"main ="+ ,"#ifdef __GHCIDE__"+ ," worked"+ ,"#else"+ ," failed"+ ,"#endif"+ ]+ expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 2), "Variable not in scope: worked")])]+ ]+ where+ expectError :: T.Text -> Cursor -> Session ()+ expectError content cursor = do+ _ <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics+ [ ( "Testing.hs",+ [(DiagnosticSeverity_Error, cursor, "error: unterminated")]+ )+ ]+ expectNoMoreDiagnostics 0.5
+ test/exe/ClientSettingsTests.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE GADTs #-}+module ClientSettingsTests (tests) where++import Control.Applicative.Combinators+import Control.Monad+import Data.Aeson (toJSON)+import qualified Data.Aeson as A+import qualified Data.Text as T+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import Test.Tasty+import TestUtils++tests :: TestTree+tests = testGroup "client settings handling"+ [ testSession "ghcide restarts shake session on config changes" $ do+ void $ skipManyTill anyMessage $ message SMethod_ClientRegisterCapability+ void $ createDoc "A.hs" "haskell" "module A where"+ waitForProgressDone+ sendNotification SMethod_WorkspaceDidChangeConfiguration+ (DidChangeConfigurationParams (toJSON (mempty :: A.Object)))+ skipManyTill anyMessage restartingBuildSession++ ]+ where+ restartingBuildSession :: Session ()+ restartingBuildSession = do+ FromServerMess SMethod_WindowLogMessage TNotificationMessage{_params = LogMessageParams{..}} <- loggingNotification+ guard $ "Restarting build session" `T.isInfixOf` _message
+ test/exe/CodeLensTests.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE GADTs #-}++module CodeLensTests (tests) where++import Control.Applicative.Combinators+import Control.Monad.IO.Class (liftIO)+import qualified Data.Aeson as A+import Data.Maybe+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+-- import Test.QuickCheck.Instances ()+import Control.Lens ((^.))+import Data.Tuple.Extra+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = testGroup "code lenses"+ [ addSigLensesTests+ ]+++addSigLensesTests :: TestTree+addSigLensesTests =+ let pragmas = "{-# OPTIONS_GHC -Wmissing-signatures -Wmissing-pattern-synonym-signatures #-}"+ moduleH exported =+ T.unlines+ [ "{-# LANGUAGE PatternSynonyms,TypeApplications,DataKinds,RankNTypes,ScopedTypeVariables,TypeOperators,GADTs,BangPatterns #-}"+ , "module Sigs(" <> exported <> ") where"+ , "import qualified Data.Complex as C"+ , "import Data.Data (Proxy (..), type (:~:) (..), mkCharType)"+ , "data T1 a where"+ , " MkT1 :: (Show b) => a -> b -> T1 a"+ ]+ before enableGHCWarnings exported (def, _) others =+ T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported, def] <> others+ after' enableGHCWarnings exported (def, sig) others =+ T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported] <> maybe [] pure sig <> [def] <> others+ createConfig mode = A.object ["haskell" A..= A.object ["plugin" A..= A.object ["ghcide-type-lenses" A..= A.object ["config" A..= A.object ["mode" A..= A.String mode]]]]]+ sigSession testName enableGHCWarnings mode exported def others = testSession testName $ do+ let originalCode = before enableGHCWarnings exported def others+ let expectedCode = after' enableGHCWarnings exported def others+ sendNotification SMethod_WorkspaceDidChangeConfiguration $ DidChangeConfigurationParams $ createConfig mode+ doc <- createDoc "Sigs.hs" "haskell" originalCode+ waitForProgressDone+ codeLenses <- getCodeLenses doc+ if not $ null $ snd def+ then do+ liftIO $ length codeLenses == 1 @? "Expected 1 code lens, but got: " <> show codeLenses+ executeCommand $ fromJust $ head codeLenses ^. L.command+ modifiedCode <- skipManyTill anyMessage (getDocumentEdit doc)+ liftIO $ expectedCode @=? modifiedCode+ else liftIO $ null codeLenses @? "Expected no code lens, but got: " <> show codeLenses+ cases =+ [ ("abc = True", "abc :: Bool")+ , ("foo a b = a + b", "foo :: Num a => a -> a -> a")+ , ("bar a b = show $ a + b", "bar :: (Show a, Num a) => a -> a -> String")+ , ("(!!!) a b = a > b", "(!!!) :: Ord a => a -> a -> Bool")+ , ("a >>>> b = a + b", "(>>>>) :: Num a => a -> a -> a")+ , ("a `haha` b = a b", "haha :: (t1 -> t2) -> t1 -> t2")+ , ("pattern Some a = Just a", "pattern Some :: a -> Maybe a")+ , ("pattern Some a <- Just a", "pattern Some :: a -> Maybe a")+ , ("pattern Some a <- Just a\n where Some a = Just a", "pattern Some :: a -> Maybe a")+ , ("pattern Some a <- Just !a\n where Some !a = Just a", "pattern Some :: a -> Maybe a")+ , ("pattern Point{x, y} = (x, y)", "pattern Point :: a -> b -> (a, b)")+ , ("pattern Point{x, y} <- (x, y)", "pattern Point :: a -> b -> (a, b)")+ , ("pattern Point{x, y} <- (x, y)\n where Point x y = (x, y)", "pattern Point :: a -> b -> (a, b)")+ , ("pattern MkT1' b = MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a")+ , ("pattern MkT1' b <- MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a")+ , ("pattern MkT1' b <- MkT1 42 b\n where MkT1' b = MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a")+ , ("qualifiedSigTest= C.realPart", "qualifiedSigTest :: C.Complex a -> a")+ , ("head = 233", "head :: Integer")+ , ("rank2Test (k :: forall a . a -> a) = (k 233 :: Int, k \"QAQ\")", "rank2Test :: (forall a. a -> a) -> (Int, " <> listOfChar <> ")")+ , ("symbolKindTest = Proxy @\"qwq\"", "symbolKindTest :: Proxy \"qwq\"")+ , ("promotedKindTest = Proxy @Nothing", if ghcVersion >= GHC96 then "promotedKindTest :: Proxy Nothing" else "promotedKindTest :: Proxy 'Nothing")+ , ("typeOperatorTest = Refl", if ghcVersion >= GHC92 then "typeOperatorTest :: forall {k} {a :: k}. a :~: a" else "typeOperatorTest :: a :~: a")+ , ("notInScopeTest = mkCharType", "notInScopeTest :: String -> Data.Data.DataType")+ ]+ in testGroup+ "add signature"+ [ testGroup "signatures are correct" [sigSession (T.unpack $ T.replace "\n" "\\n" def) False "always" "" (def, Just sig) [] | (def, sig) <- cases]+ , sigSession "exported mode works" False "exported" "xyz" ("xyz = True", Just "xyz :: Bool") (fst <$> take 3 cases)+ , testGroup+ "diagnostics mode works"+ [ sigSession "with GHC warnings" True "diagnostics" "" (second Just $ head cases) []+ , sigSession "without GHC warnings" False "diagnostics" "" (second (const Nothing) $ head cases) []+ ]+ , testSession "keep stale lens" $ do+ let content = T.unlines+ [ "module Stale where"+ , "f = _"+ ]+ doc <- createDoc "Stale.hs" "haskell" content+ oldLens <- getCodeLenses doc+ liftIO $ length oldLens @?= 1+ let edit = TextEdit (mkRange 0 4 0 5) "" -- Remove the `_`+ _ <- applyEdit doc edit+ newLens <- getCodeLenses doc+ liftIO $ newLens @?= oldLens+ ]++-- | Before ghc9, lists of Char is displayed as [Char], but with ghc9 and up, it's displayed as String+listOfChar :: T.Text+listOfChar | ghcVersion >= GHC90 = "String"+ | otherwise = "[Char]"
+ test/exe/CompletionTests.hs view
@@ -0,0 +1,576 @@++{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedLabels #-}++module CompletionTests (tests) where++import Control.Lens ((^.))+import qualified Control.Lens as Lens+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.Default+import Data.List.Extra+import Data.Maybe+import Data.Row+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.Test (waitForTypecheck)+import Development.IDE.Types.Location+import Ide.Plugin.Config+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.FilePath+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+++tests :: TestTree+tests+ = testGroup "completion"+ [+ testGroup "non local" nonLocalCompletionTests+ , testGroup "topLevel" topLevelCompletionTests+ , testGroup "local" localCompletionTests+ , testGroup "package" packageCompletionTests+ , testGroup "project" projectCompletionTests+ , testGroup "other" otherCompletionTests+ , testGroup "doc" completionDocTests+ ]++completionTest :: HasCallStack => String -> [T.Text] -> Position -> [(T.Text, CompletionItemKind, T.Text, Bool, Bool, Maybe [TextEdit])] -> TestTree+completionTest name src pos expected = testSessionWait name $ do+ docId <- createDoc "A.hs" "haskell" (T.unlines src)+ _ <- waitForDiagnostics+ compls <- getAndResolveCompletions docId pos+ let compls' = [ (_label, _kind, _insertText, _additionalTextEdits) | CompletionItem{..} <- compls]+ let emptyToMaybe x = if T.null x then Nothing else Just x+ liftIO $ sortOn (Lens.view Lens._1) (take (length expected) compls') @?=+ sortOn (Lens.view Lens._1)+ [ (l, Just k, emptyToMaybe t, at) | (l,k,t,_,_,at) <- expected]+ forM_ (zip compls expected) $ \(CompletionItem{..}, (_,_,_,expectedSig, expectedDocs, _)) -> do+ when expectedSig $+ liftIO $ assertBool ("Missing type signature: " <> T.unpack _label) (isJust _detail)+ when expectedDocs $+ liftIO $ assertBool ("Missing docs: " <> T.unpack _label) (isJust _documentation)+++topLevelCompletionTests :: [TestTree]+topLevelCompletionTests = [+ completionTest+ "variable"+ ["bar = xx", "-- | haddock", "xxx :: ()", "xxx = ()", "-- | haddock", "data Xxx = XxxCon"]+ (Position 0 8)+ [("xxx", CompletionItemKind_Function, "xxx", True, True, Nothing)+ ],+ completionTest+ "constructor"+ ["bar = xx", "-- | haddock", "xxx :: ()", "xxx = ()", "-- | haddock", "data Xxx = XxxCon"]+ (Position 0 8)+ [("xxx", CompletionItemKind_Function, "xxx", True, True, Nothing)+ ],+ completionTest+ "class method"+ ["bar = xx", "class Xxx a where", "-- | haddock", "xxx :: ()", "xxx = ()"]+ (Position 0 8)+ [("xxx", CompletionItemKind_Function, "xxx", True, True, Nothing)],+ completionTest+ "type"+ ["bar :: Xz", "zzz = ()", "-- | haddock", "data Xzz = XzzCon"]+ (Position 0 9)+ [("Xzz", CompletionItemKind_Struct, "Xzz", False, True, Nothing)],+ completionTest+ "class"+ ["bar :: Xz", "zzz = ()", "-- | haddock", "class Xzz a"]+ (Position 0 9)+ [("Xzz", CompletionItemKind_Interface, "Xzz", False, True, Nothing)],+ completionTest+ "records"+ ["data Person = Person { _personName:: String, _personAge:: Int}", "bar = Person { _pers }" ]+ (Position 1 19)+ [("_personName", CompletionItemKind_Function, "_personName", False, True, Nothing),+ ("_personAge", CompletionItemKind_Function, "_personAge", False, True, Nothing)],+ completionTest+ "recordsConstructor"+ ["data XxRecord = XyRecord { x:: String, y:: Int}", "bar = Xy" ]+ (Position 1 19)+ [("XyRecord", CompletionItemKind_Constructor, "XyRecord", False, True, Nothing),+ ("XyRecord", CompletionItemKind_Snippet, "XyRecord {x=${1:_x}, y=${2:_y}}", False, True, Nothing)]+ ]++localCompletionTests :: [TestTree]+localCompletionTests = [+ completionTest+ "argument"+ ["bar (Just abcdef) abcdefg = abcd"]+ (Position 0 32)+ [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing),+ ("abcdefg", CompletionItemKind_Function , "abcdefg", True, False, Nothing)+ ],+ completionTest+ "let"+ ["bar = let (Just abcdef) = undefined"+ ," abcdefg = let abcd = undefined in undefined"+ ," in abcd"+ ]+ (Position 2 15)+ [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing),+ ("abcdefg", CompletionItemKind_Function , "abcdefg", True, False, Nothing)+ ],+ completionTest+ "where"+ ["bar = abcd"+ ," where (Just abcdef) = undefined"+ ," abcdefg = let abcd = undefined in undefined"+ ]+ (Position 0 10)+ [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing),+ ("abcdefg", CompletionItemKind_Function , "abcdefg", True, False, Nothing)+ ],+ completionTest+ "do/1"+ ["bar = do"+ ," Just abcdef <- undefined"+ ," abcd"+ ," abcdefg <- undefined"+ ," pure ()"+ ]+ (Position 2 6)+ [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing)+ ],+ completionTest+ "do/2"+ ["bar abcde = do"+ ," Just [(abcdef,_)] <- undefined"+ ," abcdefg <- undefined"+ ," let abcdefgh = undefined"+ ," (Just [abcdefghi]) = undefined"+ ," abcd"+ ," where"+ ," abcdefghij = undefined"+ ]+ (Position 5 8)+ [("abcde", CompletionItemKind_Function, "abcde", True, False, Nothing)+ ,("abcdefghij", CompletionItemKind_Function, "abcdefghij", True, False, Nothing)+ ,("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing)+ ,("abcdefg", CompletionItemKind_Function, "abcdefg", True, False, Nothing)+ ,("abcdefgh", CompletionItemKind_Function, "abcdefgh", True, False, Nothing)+ ,("abcdefghi", CompletionItemKind_Function, "abcdefghi", True, False, Nothing)+ ],+ completionTest+ "type family"+ ["{-# LANGUAGE DataKinds, TypeFamilies #-}"+ ,"type family Bar a"+ ,"a :: Ba"+ ]+ (Position 2 7)+ [("Bar", CompletionItemKind_Struct, "Bar", True, False, Nothing)+ ],+ completionTest+ "class method"+ [+ "class Test a where"+ , " abcd :: a -> ()"+ , " abcde :: a -> Int"+ , "instance Test Int where"+ , " abcd = abc"+ ]+ (Position 4 14)+ [("abcd", CompletionItemKind_Function, "abcd", True, False, Nothing)+ ,("abcde", CompletionItemKind_Function, "abcde", True, False, Nothing)+ ],+ testSessionWait "incomplete entries" $ do+ let src a = "data Data = " <> a+ doc <- createDoc "A.hs" "haskell" $ src "AAA"+ void $ waitForTypecheck doc+ let editA rhs =+ changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ src rhs]+ editA "AAAA"+ void $ waitForTypecheck doc+ editA "AAAAA"+ void $ waitForTypecheck doc++ compls <- getCompletions doc (Position 0 15)+ liftIO $ filter ("AAA" `T.isPrefixOf`) (mapMaybe _insertText compls) @?= ["AAAAA"]+ pure ()+ ]++nonLocalCompletionTests :: [TestTree]+nonLocalCompletionTests =+ [ brokenForWinGhc $ completionTest+ "variable"+ ["module A where", "f = hea"]+ (Position 1 7)+ [("head", CompletionItemKind_Function, "head", True, True, Nothing)],+ completionTest+ "constructor"+ ["{-# OPTIONS_GHC -Wall #-}", "module A where", "f = True"]+ (Position 2 8)+ [ ("True", CompletionItemKind_Constructor, "True", True, True, Nothing)+ ],+ brokenForWinGhc $ completionTest+ "type"+ ["{-# OPTIONS_GHC -Wall #-}", "module A () where", "f :: Boo", "f = True"]+ (Position 2 8)+ [ ("Bool", CompletionItemKind_Struct, "Bool", True, True, Nothing)+ ],+ completionTest+ "qualified"+ ["{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", "f = Prelude.hea"]+ (Position 2 15)+ [ ("head", CompletionItemKind_Function, "head", True, True, Nothing)+ ],+ completionTest+ "duplicate import"+ ["module A where", "import Data.List", "import Data.List", "f = permu"]+ (Position 3 9)+ [ ("permutations", CompletionItemKind_Function, "permutations", False, False, Nothing)+ ],+ completionTest+ "dont show hidden items"+ [ "{-# LANGUAGE NoImplicitPrelude #-}",+ "module A where",+ "import Control.Monad hiding (join)",+ "f = joi"+ ]+ (Position 3 6)+ [],+ testGroup "ordering"+ [completionTest "qualified has priority"+ ["module A where"+ ,"import qualified Data.ByteString as BS"+ ,"f = BS.read"+ ]+ (Position 2 10)+ [("readFile", CompletionItemKind_Function, "readFile", True, True, Nothing)]+ ],+ -- we need this test to make sure the ghcide completions module does not return completions for language pragmas. this functionality is turned on in hls+ completionTest+ "do not show pragma completions"+ [ "{-# LANGUAGE ",+ "{module A where}",+ "main = return ()"+ ]+ (Position 0 13)+ []+ ]+ where+ brokenForWinGhc = knownBrokenFor (BrokenSpecific Windows [GHC810, GHC90, GHC92, GHC94, GHC96]) "Windows has strange things in scope for some reason"++otherCompletionTests :: [TestTree]+otherCompletionTests = [+ completionTest+ "keyword"+ ["module A where", "f = newty"]+ (Position 1 9)+ [("newtype", CompletionItemKind_Keyword, "", False, False, Nothing)],+ completionTest+ "type context"+ [ "{-# OPTIONS_GHC -Wunused-binds #-}",+ "module A () where",+ "f = f",+ "g :: Intege"+ ]+ -- At this point the module parses but does not typecheck.+ -- This should be sufficient to detect that we are in a+ -- type context and only show the completion to the type.+ (Position 3 11)+ [("Integer", CompletionItemKind_Struct, "Integer", True, True, Nothing)],++ testSession "duplicate record fields" $ do+ void $+ createDoc "B.hs" "haskell" $+ T.unlines+ [ "{-# LANGUAGE DuplicateRecordFields #-}",+ "module B where",+ "newtype Foo = Foo { member :: () }",+ "newtype Bar = Bar { member :: () }"+ ]+ docA <-+ createDoc "A.hs" "haskell" $+ T.unlines+ [ "module A where",+ "import B",+ "memb"+ ]+ _ <- waitForDiagnostics+ compls <- getCompletions docA $ Position 2 4+ let compls' = [txt | CompletionItem {_insertText = Just txt, ..} <- compls, _label == "member"]+ liftIO $ take 2 compls' @?= ["member"],++ testSessionWait "maxCompletions" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS_GHC -Wunused-binds #-}",+ "module A () where",+ "a = Prelude."+ ]+ _ <- waitForDiagnostics+ compls <- getCompletions doc (Position 3 13)+ liftIO $ length compls @?= maxCompletions def+ ]++packageCompletionTests :: [TestTree]+packageCompletionTests =+ [ testSession' "fromList" $ \dir -> do+ liftIO $ writeFile (dir </> "hie.yaml")+ "cradle: {direct: {arguments: [-hide-all-packages, -package, base, A]}}"+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS_GHC -Wunused-binds #-}",+ "module A () where",+ "a = fromList"+ ]+ _ <- waitForDiagnostics+ compls <- getCompletions doc (Position 2 12)+ let compls' =+ [T.drop 1 $ T.dropEnd 3 d+ | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown d)), _label}+ <- compls+ , _label == "fromList"+ ]+ liftIO $ take 3 (sort compls') @?=+ map ("Defined in "<>) (+ [ "'Data.List.NonEmpty"+ , "'GHC.Exts"+ ] ++ if ghcVersion >= GHC94 then [ "'GHC.IsList" ] else [])++ , testSessionWait "Map" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS_GHC -Wunused-binds #-}",+ "module A () where",+ "a :: Map"+ ]+ _ <- waitForDiagnostics+ compls <- getCompletions doc (Position 2 7)+ let compls' =+ [T.drop 1 $ T.dropEnd 3 d+ | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown d)), _label}+ <- compls+ , _label == "Map"+ ]+ liftIO $ take 3 (sort compls') @?=+ map ("Defined in "<>)+ [ "'Data.Map"+ , "'Data.Map.Lazy"+ , "'Data.Map.Strict"+ ]+ , testSessionWait "no duplicates" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS_GHC -Wunused-binds #-}",+ "module A () where",+ "import GHC.Exts(fromList)",+ "a = fromList"+ ]+ _ <- waitForDiagnostics+ compls <- getCompletions doc (Position 3 13)+ let duplicate =+ filter+ (\case+ CompletionItem+ { _insertText = Just "fromList"+ , _documentation =+ Just (InR (MarkupContent MarkupKind_Markdown d))+ } ->+ "GHC.Exts" `T.isInfixOf` d+ _ -> False+ ) compls+ liftIO $ length duplicate @?= 1++ , testSessionWait "non-local before global" $ do+ -- non local completions are more specific+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS_GHC -Wunused-binds #-}",+ "module A () where",+ "import GHC.Exts(fromList)",+ "a = fromList"+ ]+ _ <- waitForDiagnostics+ compls <- getCompletions doc (Position 3 13)+ let compls' =+ [_insertText+ | CompletionItem {_label, _insertText} <- compls+ , _label == "fromList"+ ]+ liftIO $ take 3 compls' @?=+ map Just ["fromList"]+ ]++projectCompletionTests :: [TestTree]+projectCompletionTests =+ [ testSession' "from hiedb" $ \dir-> do+ liftIO $ writeFile (dir </> "hie.yaml")+ "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"+ _ <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A (anidentifier) where",+ "anidentifier = ()"+ ]+ _ <- waitForDiagnostics+ -- Note that B does not import A+ doc <- createDoc "B.hs" "haskell" $ T.unlines+ [ "module B where",+ "b = anidenti"+ ]+ compls <- getCompletions doc (Position 1 10)+ let compls' =+ [T.drop 1 $ T.dropEnd 3 d+ | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown d)), _label}+ <- compls+ , _label == "anidentifier"+ ]+ liftIO $ compls' @?= ["Defined in 'A"],+ testSession' "auto complete project imports" $ \dir-> do+ liftIO $ writeFile (dir </> "hie.yaml")+ "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"ALocalModule\", \"B\"]}}"+ _ <- createDoc "ALocalModule.hs" "haskell" $ T.unlines+ [ "module ALocalModule (anidentifier) where",+ "anidentifier = ()"+ ]+ _ <- waitForDiagnostics+ -- Note that B does not import A+ doc <- createDoc "B.hs" "haskell" $ T.unlines+ [ "module B where",+ "import ALocal"+ ]+ compls <- getCompletions doc (Position 1 13)+ let item = head $ filter ((== "ALocalModule") . (^. L.label)) compls+ liftIO $ do+ item ^. L.label @?= "ALocalModule",+ testSession' "auto complete functions from qualified imports without alias" $ \dir-> do+ liftIO $ writeFile (dir </> "hie.yaml")+ "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"+ _ <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A (anidentifier) where",+ "anidentifier = ()"+ ]+ _ <- waitForDiagnostics+ doc <- createDoc "B.hs" "haskell" $ T.unlines+ [ "module B where",+ "import qualified A",+ "A."+ ]+ compls <- getCompletions doc (Position 2 2)+ let item = head compls+ liftIO $ do+ item ^. L.label @?= "anidentifier",+ testSession' "auto complete functions from qualified imports with alias" $ \dir-> do+ liftIO $ writeFile (dir </> "hie.yaml")+ "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"+ _ <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A (anidentifier) where",+ "anidentifier = ()"+ ]+ _ <- waitForDiagnostics+ doc <- createDoc "B.hs" "haskell" $ T.unlines+ [ "module B where",+ "import qualified A as Alias",+ "foo = Alias."+ ]+ compls <- getCompletions doc (Position 2 12)+ let item = head compls+ liftIO $ do+ item ^. L.label @?= "anidentifier"+ ]++completionDocTests :: [TestTree]+completionDocTests =+ [ testSession "local define" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "foo = ()"+ , "bar = fo"+ ]+ let expected = "*Defined at line 2, column 1 in this module*\n"+ test doc (Position 2 8) "foo" Nothing [expected]+ , testSession "local empty doc" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "foo = ()"+ , "bar = fo"+ ]+ test doc (Position 2 8) "foo" Nothing ["*Defined at line 2, column 1 in this module*\n"]+ , testSession "local single line doc without newline" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "-- |docdoc"+ , "foo = ()"+ , "bar = fo"+ ]+ test doc (Position 3 8) "foo" Nothing ["*Defined at line 3, column 1 in this module*\n* * *\n\n\ndocdoc\n"]+ , testSession "local multi line doc with newline" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "-- | abcabc"+ , "--"+ , "foo = ()"+ , "bar = fo"+ ]+ test doc (Position 4 8) "foo" Nothing ["*Defined at line 4, column 1 in this module*\n* * *\n\n\nabcabc\n"]+ , testSession "local multi line doc without newline" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "-- | abcabc"+ , "--"+ , "--def"+ , "foo = ()"+ , "bar = fo"+ ]+ test doc (Position 5 8) "foo" Nothing ["*Defined at line 5, column 1 in this module*\n* * *\n\n\nabcabc \n\ndef\n"]+ , testSession "extern empty doc" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "foo = od"+ ]+ let expected = "*Imported from 'Prelude'*\n"+ test doc (Position 1 8) "odd" (Just $ T.length expected) [expected]+ , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern single line doc without '\\n'" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "foo = no"+ ]+ let expected = "*Imported from 'Prelude'*\n* * *\n\n\nBoolean \"not\"\n"+ test doc (Position 1 8) "not" (Just $ T.length expected) [expected]+ , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern mulit line doc" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "foo = i"+ ]+ let expected = "*Imported from 'Prelude'*\n* * *\n\n\nIdentity function. \n```haskell\nid x = x\n```\n"+ test doc (Position 1 7) "id" (Just $ T.length expected) [expected]+ , testSession "extern defined doc" $ do+ doc <- createDoc "A.hs" "haskell" $ T.unlines+ [ "module A where"+ , "foo = i"+ ]+ let expected = "*Imported from 'Prelude'*\n"+ test doc (Position 1 7) "id" (Just $ T.length expected) [expected]+ ]+ where+ brokenForGhc9 = knownBrokenFor (BrokenForGHC [GHC90, GHC92, GHC94, GHC96]) "Completion doc doesn't support ghc9"+ brokenForWinGhc90 = knownBrokenFor (BrokenSpecific Windows [GHC90]) "Extern doc doesn't support Windows for ghc9.2"+ -- https://gitlab.haskell.org/ghc/ghc/-/issues/20903+ brokenForMacGhc9 = knownBrokenFor (BrokenSpecific MacOS [GHC90, GHC92, GHC94, GHC96]) "Extern doc doesn't support MacOS for ghc9"+ test doc pos label mn expected = do+ _ <- waitForDiagnostics+ compls <- getCompletions doc pos+ rcompls <- forM compls $ \item -> do+ if isJust (item ^. L.data_)+ then do+ rsp <- request SMethod_CompletionItemResolve item+ case rsp ^. L.result of+ Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err)+ Right x -> pure x+ else pure item+ let compls' = [+ -- We ignore doc uris since it points to the local path which determined by specific machines+ case mn of+ Nothing -> txt+ Just n -> T.take n txt+ | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown txt)), ..} <- rcompls+ , _label == label+ ]+ liftIO $ compls' @?= expected
+ test/exe/CradleTests.hs view
@@ -0,0 +1,219 @@++{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedLabels #-}++module CradleTests (tests) where++import Control.Applicative.Combinators+import Control.Monad.IO.Class (liftIO)+import Data.Row+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.GHC.Util+import Development.IDE.Test (expectDiagnostics,+ expectDiagnosticsWithTags,+ expectNoMoreDiagnostics,+ isReferenceReady,+ waitForAction)+import Development.IDE.Types.Location+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.FilePath+import System.IO.Extra hiding (withTempDir)+-- import Test.QuickCheck.Instances ()+import Control.Lens ((^.))+import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..))+import GHC.TypeLits (symbolVal)+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+++tests :: TestTree+tests = testGroup "cradle"+ [testGroup "dependencies" [sessionDepsArePickedUp]+ ,testGroup "ignore-fatal" [ignoreFatalWarning]+ ,testGroup "loading" [loadCradleOnlyonce, retryFailedCradle]+ ,testGroup "multi" [simpleMultiTest, simpleMultiTest2, simpleMultiTest3, simpleMultiDefTest]+ ,testGroup "sub-directory" [simpleSubDirectoryTest]+ ]++loadCradleOnlyonce :: TestTree+loadCradleOnlyonce = testGroup "load cradle only once"+ [ testSession' "implicit" implicit+ , testSession' "direct" direct+ ]+ where+ direct dir = do+ liftIO $ writeFileUTF8 (dir </> "hie.yaml")+ "cradle: {direct: {arguments: []}}"+ test dir+ implicit dir = test dir+ test _dir = do+ doc <- createDoc "B.hs" "haskell" "module B where\nimport Data.Foo"+ msgs <- someTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics))+ liftIO $ length msgs @?= 1+ changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ "module B where\nimport Data.Maybe"]+ msgs <- manyTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics))+ liftIO $ length msgs @?= 0+ _ <- createDoc "A.hs" "haskell" "module A where\nimport LoadCradleBar"+ msgs <- manyTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message SMethod_TextDocumentPublishDiagnostics))+ liftIO $ length msgs @?= 0++retryFailedCradle :: TestTree+retryFailedCradle = testSession' "retry failed" $ \dir -> do+ -- The false cradle always fails+ let hieContents = "cradle: {bios: {shell: \"false\"}}"+ hiePath = dir </> "hie.yaml"+ liftIO $ writeFile hiePath hieContents+ let aPath = dir </> "A.hs"+ doc <- createDoc aPath "haskell" "main = return ()"+ WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc+ liftIO $ "Test assumption failed: cradle should error out" `assertBool` not ideResultSuccess++ -- Fix the cradle and typecheck again+ let validCradle = "cradle: {bios: {shell: \"echo A.hs\"}}"+ liftIO $ writeFileUTF8 hiePath $ T.unpack validCradle+ sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $+ [FileEvent (filePathToUri $ dir </> "hie.yaml") FileChangeType_Changed ]++ WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc+ liftIO $ "No joy after fixing the cradle" `assertBool` ideResultSuccess+++cradleLoadedMessage :: Session FromServerMessage+cradleLoadedMessage = satisfy $ \case+ FromServerMess (SMethod_CustomMethod p) (NotMess _) -> symbolVal p == cradleLoadedMethod+ _ -> False++cradleLoadedMethod :: String+cradleLoadedMethod = "ghcide/cradle/loaded"++ignoreFatalWarning :: TestTree+ignoreFatalWarning = testCase "ignore-fatal-warning" $ runWithExtraFiles "ignore-fatal" $ \dir -> do+ let srcPath = dir </> "IgnoreFatal.hs"+ src <- liftIO $ readFileUtf8 srcPath+ _ <- createDoc srcPath "haskell" src+ expectNoMoreDiagnostics 5++simpleSubDirectoryTest :: TestTree+simpleSubDirectoryTest =+ testCase "simple-subdirectory" $ runWithExtraFiles "cabal-exe" $ \dir -> do+ let mainPath = dir </> "a/src/Main.hs"+ mainSource <- liftIO $ readFileUtf8 mainPath+ _mdoc <- createDoc mainPath "haskell" mainSource+ expectDiagnosticsWithTags+ [("a/src/Main.hs", [(DiagnosticSeverity_Warning,(2,0), "Top-level binding", Nothing)]) -- So that we know P has been loaded+ ]+ expectNoMoreDiagnostics 0.5++simpleMultiTest :: TestTree+simpleMultiTest = testCase "simple-multi-test" $ withLongTimeout $ runWithExtraFiles "multi" $ \dir -> do+ let aPath = dir </> "a/A.hs"+ bPath = dir </> "b/B.hs"+ adoc <- openDoc aPath "haskell"+ bdoc <- openDoc bPath "haskell"+ WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" adoc+ liftIO $ assertBool "A should typecheck" ideResultSuccess+ WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" bdoc+ liftIO $ assertBool "B should typecheck" ideResultSuccess+ locs <- getDefinitions bdoc (Position 2 7)+ let fooL = mkL (adoc ^. L.uri) 2 0 2 3+ checkDefs locs (pure [fooL])+ expectNoMoreDiagnostics 0.5++-- Like simpleMultiTest but open the files in the other order+simpleMultiTest2 :: TestTree+simpleMultiTest2 = testCase "simple-multi-test2" $ runWithExtraFiles "multi" $ \dir -> do+ let aPath = dir </> "a/A.hs"+ bPath = dir </> "b/B.hs"+ bdoc <- openDoc bPath "haskell"+ WaitForIdeRuleResult {} <- waitForAction "TypeCheck" bdoc+ TextDocumentIdentifier auri <- openDoc aPath "haskell"+ skipManyTill anyMessage $ isReferenceReady aPath+ locs <- getDefinitions bdoc (Position 2 7)+ let fooL = mkL auri 2 0 2 3+ checkDefs locs (pure [fooL])+ expectNoMoreDiagnostics 0.5++-- Now with 3 components+simpleMultiTest3 :: TestTree+simpleMultiTest3 =+ testCase "simple-multi-test3" $ runWithExtraFiles "multi" $ \dir -> do+ let aPath = dir </> "a/A.hs"+ bPath = dir </> "b/B.hs"+ cPath = dir </> "c/C.hs"+ bdoc <- openDoc bPath "haskell"+ WaitForIdeRuleResult {} <- waitForAction "TypeCheck" bdoc+ TextDocumentIdentifier auri <- openDoc aPath "haskell"+ skipManyTill anyMessage $ isReferenceReady aPath+ cdoc <- openDoc cPath "haskell"+ WaitForIdeRuleResult {} <- waitForAction "TypeCheck" cdoc+ locs <- getDefinitions cdoc (Position 2 7)+ let fooL = mkL auri 2 0 2 3+ checkDefs locs (pure [fooL])+ expectNoMoreDiagnostics 0.5++-- Like simpleMultiTest but open the files in component 'a' in a separate session+simpleMultiDefTest :: TestTree+simpleMultiDefTest = testCase "simple-multi-def-test" $ runWithExtraFiles "multi" $ \dir -> do+ let aPath = dir </> "a/A.hs"+ bPath = dir </> "b/B.hs"+ adoc <- liftIO $ runInDir dir $ do+ aSource <- liftIO $ readFileUtf8 aPath+ adoc <- createDoc aPath "haskell" aSource+ skipManyTill anyMessage $ isReferenceReady aPath+ closeDoc adoc+ pure adoc+ bSource <- liftIO $ readFileUtf8 bPath+ bdoc <- createDoc bPath "haskell" bSource+ locs <- getDefinitions bdoc (Position 2 7)+ let fooL = mkL (adoc ^. L.uri) 2 0 2 3+ checkDefs locs (pure [fooL])+ expectNoMoreDiagnostics 0.5+++sessionDepsArePickedUp :: TestTree+sessionDepsArePickedUp = testSession'+ "session-deps-are-picked-up"+ $ \dir -> do+ liftIO $+ writeFileUTF8+ (dir </> "hie.yaml")+ "cradle: {direct: {arguments: []}}"+ -- Open without OverloadedStrings and expect an error.+ doc <- createDoc "Foo.hs" "haskell" fooContent+ expectDiagnostics $+ if ghcVersion >= GHC90+ -- String vs [Char] causes this change in error message+ then [("Foo.hs", [(DiagnosticSeverity_Error, (3, 6), "Couldn't match type")])]+ else [("Foo.hs", [(DiagnosticSeverity_Error, (3, 6), "Couldn't match expected type")])]+ -- Update hie.yaml to enable OverloadedStrings.+ liftIO $+ writeFileUTF8+ (dir </> "hie.yaml")+ "cradle: {direct: {arguments: [-XOverloadedStrings]}}"+ sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $+ [FileEvent (filePathToUri $ dir </> "hie.yaml") FileChangeType_Changed ]+ -- Send change event.+ let change =+ TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 4 0) (Position 4 0)+ .+ #rangeLength .== Nothing+ .+ #text .== "\n"+ changeDoc doc [change]+ -- Now no errors.+ expectDiagnostics [("Foo.hs", [])]+ where+ fooContent =+ T.unlines+ [ "module Foo where",+ "import Data.Text",+ "foo :: Text",+ "foo = \"hello\""+ ]
+ test/exe/DependentFileTest.hs view
@@ -0,0 +1,62 @@++{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedLabels #-}++module DependentFileTest (tests) where++import Control.Monad.IO.Class (liftIO)+import Data.Row+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.Test (expectDiagnostics)+import Development.IDE.Types.Location+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.FilePath+import Test.Tasty+import TestUtils++tests :: TestTree+tests = testGroup "addDependentFile"+ [testGroup "file-changed" [testSession' "test" test]+ ]+ where+ test dir = do+ -- If the file contains B then no type error+ -- otherwise type error+ let depFilePath = dir </> "dep-file.txt"+ liftIO $ writeFile depFilePath "A"+ let fooContent = T.unlines+ [ "{-# LANGUAGE TemplateHaskell #-}"+ , "module Foo where"+ , "import Language.Haskell.TH.Syntax"+ , "foo :: Int"+ , "foo = 1 + $(do"+ , " qAddDependentFile \"dep-file.txt\""+ , " f <- qRunIO (readFile \"dep-file.txt\")"+ , " if f == \"B\" then [| 1 |] else lift f)"+ ]+ let bazContent = T.unlines ["module Baz where", "import Foo ()"]+ _ <- createDoc "Foo.hs" "haskell" fooContent+ doc <- createDoc "Baz.hs" "haskell" bazContent+ expectDiagnostics $+ if ghcVersion >= GHC90+ -- String vs [Char] causes this change in error message+ then [("Foo.hs", [(DiagnosticSeverity_Error, if ghcVersion >= GHC92 then (4,11) else (4, 6), "Couldn't match type")])]+ else [("Foo.hs", [(DiagnosticSeverity_Error, (4, 6), "Couldn't match expected type")])]+ -- Now modify the dependent file+ liftIO $ writeFile depFilePath "B"+ sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $+ [FileEvent (filePathToUri "dep-file.txt") FileChangeType_Changed ]++ -- Modifying Baz will now trigger Foo to be rebuilt as well+ let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 2 0) (Position 2 6)+ .+ #rangeLength .== Nothing+ .+ #text .== "f = ()"+ changeDoc doc [change]+ expectDiagnostics [("Foo.hs", [])]
+ test/exe/DiagnosticTests.hs view
@@ -0,0 +1,565 @@++{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedLabels #-}++module DiagnosticTests (tests) where++import Control.Applicative.Combinators+import qualified Control.Lens as Lens+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.List.Extra+import Data.Row+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.GHC.Util+import Development.IDE.Test (diagnostic,+ expectCurrentDiagnostics,+ expectDiagnostics,+ expectDiagnosticsWithTags,+ expectNoMoreDiagnostics,+ flushMessages, waitForAction)+import Development.IDE.Types.Location+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.Directory+import System.FilePath+import System.IO.Extra hiding (withTempDir)+-- import Test.QuickCheck.Instances ()+import Control.Lens ((^.))+import Control.Monad.Extra (whenJust)+import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..))+import System.Time.Extra+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = testGroup "diagnostics"+ [ testSessionWait "fix syntax error" $ do+ let content = T.unlines [ "module Testing wher" ]+ doc <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "parse error")])]+ let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 15) (Position 0 19)+ .+ #rangeLength .== Nothing+ .+ #text .== "where"+ changeDoc doc [change]+ expectDiagnostics [("Testing.hs", [])]+ , testSessionWait "introduce syntax error" $ do+ let content = T.unlines [ "module Testing where" ]+ doc <- createDoc "Testing.hs" "haskell" content+ void $ skipManyTill anyMessage (message SMethod_WindowWorkDoneProgressCreate)+ waitForProgressBegin+ let change = TextDocumentContentChangeEvent$ InL $ #range .== Range (Position 0 15) (Position 0 18)+ .+ #rangeLength .== Nothing+ .+ #text .== "wher"+ changeDoc doc [change]+ expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "parse error")])]+ , testSessionWait "update syntax error" $ do+ let content = T.unlines [ "module Testing(missing) where" ]+ doc <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "Not in scope: 'missing'")])]+ let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 15) (Position 0 16)+ .+ #rangeLength .== Nothing+ .+ #text .== "l"+ changeDoc doc [change]+ expectDiagnostics [("Testing.hs", [(DiagnosticSeverity_Error, (0, 15), "Not in scope: 'lissing'")])]+ , testSessionWait "variable not in scope" $ do+ let content = T.unlines+ [ "module Testing where"+ , "foo :: Int -> Int -> Int"+ , "foo a _b = a + ab"+ , "bar :: Int -> Int -> Int"+ , "bar _a b = cd + b"+ ]+ _ <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics+ [ ( "Testing.hs"+ , [ (DiagnosticSeverity_Error, (2, 15), "Variable not in scope: ab")+ , (DiagnosticSeverity_Error, (4, 11), "Variable not in scope: cd")+ ]+ )+ ]+ , testSessionWait "type error" $ do+ let content = T.unlines+ [ "module Testing where"+ , "foo :: Int -> String -> Int"+ , "foo a b = a + b"+ ]+ _ <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics+ [ ( "Testing.hs"+ , [(DiagnosticSeverity_Error, (2, 14), "Couldn't match type '[Char]' with 'Int'")]+ )+ ]+ , testSessionWait "typed hole" $ do+ let content = T.unlines+ [ "module Testing where"+ , "foo :: Int -> String"+ , "foo a = _ a"+ ]+ _ <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics+ [ ( "Testing.hs"+ , [(DiagnosticSeverity_Error, (2, 8), "Found hole: _ :: Int -> String")]+ )+ ]++ , testGroup "deferral" $+ let sourceA a = T.unlines+ [ "module A where"+ , "a :: Int"+ , "a = " <> a]+ sourceB = T.unlines+ [ "module B where"+ , "import A ()"+ , "b :: Float"+ , "b = True"]+ bMessage = "Couldn't match expected type 'Float' with actual type 'Bool'"+ expectedDs aMessage =+ [ ("A.hs", [(DiagnosticSeverity_Error, (2,4), aMessage)])+ , ("B.hs", [(DiagnosticSeverity_Error, (3,4), bMessage)])]+ deferralTest title binding msg = testSessionWait title $ do+ _ <- createDoc "A.hs" "haskell" $ sourceA binding+ _ <- createDoc "B.hs" "haskell" sourceB+ expectDiagnostics $ expectedDs msg+ in+ [ deferralTest "type error" "True" "Couldn't match expected type"+ , deferralTest "typed hole" "_" "Found hole"+ , deferralTest "out of scope var" "unbound" "Variable not in scope"+ ]++ , testSessionWait "remove required module" $ do+ let contentA = T.unlines [ "module ModuleA where" ]+ docA <- createDoc "ModuleA.hs" "haskell" contentA+ let contentB = T.unlines+ [ "module ModuleB where"+ , "import ModuleA"+ ]+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 0 0) (Position 0 20)+ .+ #rangeLength .== Nothing+ .+ #text .== ""+ changeDoc docA [change]+ expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Error, (1, 0), "Could not find module")])]+ , testSessionWait "add missing module" $ do+ let contentB = T.unlines+ [ "module ModuleB where"+ , "import ModuleA ()"+ ]+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Error, (1, 7), "Could not find module")])]+ let contentA = T.unlines [ "module ModuleA where" ]+ _ <- createDoc "ModuleA.hs" "haskell" contentA+ expectDiagnostics [("ModuleB.hs", [])]+ , testCase "add missing module (non workspace)" $+ -- By default lsp-test sends FileWatched notifications for all files, which we don't want+ -- as non workspace modules will not be watched by the LSP server.+ -- To work around this, we tell lsp-test that our client doesn't have the+ -- FileWatched capability, which is enough to disable the notifications+ withTempDir $ \tmpDir -> runInDir'' lspTestCapsNoFileWatches tmpDir "." "." [] $ do+ let contentB = T.unlines+ [ "module ModuleB where"+ , "import ModuleA ()"+ ]+ _ <- createDoc (tmpDir </> "ModuleB.hs") "haskell" contentB+ expectDiagnostics [(tmpDir </> "ModuleB.hs", [(DiagnosticSeverity_Error, (1, 7), "Could not find module")])]+ let contentA = T.unlines [ "module ModuleA where" ]+ _ <- createDoc (tmpDir </> "ModuleA.hs") "haskell" contentA+ expectDiagnostics [(tmpDir </> "ModuleB.hs", [])]+ , testSessionWait "cyclic module dependency" $ do+ let contentA = T.unlines+ [ "module ModuleA where"+ , "import ModuleB"+ ]+ let contentB = T.unlines+ [ "module ModuleB where"+ , "import ModuleA"+ ]+ _ <- createDoc "ModuleA.hs" "haskell" contentA+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ expectDiagnostics+ [ ( "ModuleA.hs"+ , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]+ )+ , ( "ModuleB.hs"+ , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]+ )+ ]+ , testSession' "deeply nested cyclic module dependency" $ \path -> do+ let contentA = unlines+ [ "module ModuleA where" , "import ModuleB" ]+ let contentB = unlines+ [ "module ModuleB where" , "import ModuleA" ]+ let contentC = unlines+ [ "module ModuleC where" , "import ModuleB" ]+ let contentD = T.unlines+ [ "module ModuleD where" , "import ModuleC" ]+ cradle =+ "cradle: {direct: {arguments: [ModuleA, ModuleB, ModuleC, ModuleD]}}"+ liftIO $ writeFile (path </> "ModuleA.hs") contentA+ liftIO $ writeFile (path </> "ModuleB.hs") contentB+ liftIO $ writeFile (path </> "ModuleC.hs") contentC+ liftIO $ writeFile (path </> "hie.yaml") cradle+ _ <- createDoc "ModuleD.hs" "haskell" contentD+ expectDiagnostics+ [ ( "ModuleB.hs"+ , [(DiagnosticSeverity_Error, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]+ )+ ]+ , testSessionWait "cyclic module dependency with hs-boot" $ do+ let contentA = T.unlines+ [ "module ModuleA where"+ , "import {-# SOURCE #-} ModuleB"+ ]+ let contentB = T.unlines+ [ "{-# OPTIONS -Wmissing-signatures#-}"+ , "module ModuleB where"+ , "import ModuleA"+ -- introduce an artificial diagnostic+ , "foo = ()"+ ]+ let contentBboot = T.unlines+ [ "module ModuleB where"+ ]+ _ <- createDoc "ModuleA.hs" "haskell" contentA+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ _ <- createDoc "ModuleB.hs-boot" "haskell" contentBboot+ expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding")])]+ , testSessionWait "correct reference used with hs-boot" $ do+ let contentB = T.unlines+ [ "module ModuleB where"+ , "import {-# SOURCE #-} ModuleA()"+ ]+ let contentA = T.unlines+ [ "module ModuleA where"+ , "import ModuleB()"+ , "x = 5"+ ]+ let contentAboot = T.unlines+ [ "module ModuleA where"+ ]+ let contentC = T.unlines+ [ "{-# OPTIONS -Wmissing-signatures #-}"+ , "module ModuleC where"+ , "import ModuleA"+ -- this reference will fail if it gets incorrectly+ -- resolved to the hs-boot file+ , "y = x"+ ]+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ _ <- createDoc "ModuleA.hs" "haskell" contentA+ _ <- createDoc "ModuleA.hs-boot" "haskell" contentAboot+ _ <- createDoc "ModuleC.hs" "haskell" contentC+ expectDiagnostics [("ModuleC.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding")])]+ , testSessionWait "redundant import" $ do+ let contentA = T.unlines ["module ModuleA where"]+ let contentB = T.unlines+ [ "{-# OPTIONS_GHC -Wunused-imports #-}"+ , "module ModuleB where"+ , "import ModuleA"+ ]+ _ <- createDoc "ModuleA.hs" "haskell" contentA+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ expectDiagnosticsWithTags+ [ ( "ModuleB.hs"+ , [(DiagnosticSeverity_Warning, (2, 0), "The import of 'ModuleA' is redundant", Just DiagnosticTag_Unnecessary)]+ )+ ]+ , testSessionWait "redundant import even without warning" $ do+ let contentA = T.unlines ["module ModuleA where"]+ let contentB = T.unlines+ [ "{-# OPTIONS_GHC -Wno-unused-imports -Wmissing-signatures #-}"+ , "module ModuleB where"+ , "import ModuleA"+ -- introduce an artificial warning for testing purposes+ , "foo = ()"+ ]+ _ <- createDoc "ModuleA.hs" "haskell" contentA+ _ <- createDoc "ModuleB.hs" "haskell" contentB+ expectDiagnostics [("ModuleB.hs", [(DiagnosticSeverity_Warning, (3,0), "Top-level binding")])]+ , testSessionWait "package imports" $ do+ let thisDataListContent = T.unlines+ [ "module Data.List where"+ , "x :: Integer"+ , "x = 123"+ ]+ let mainContent = T.unlines+ [ "{-# LANGUAGE PackageImports #-}"+ , "module Main where"+ , "import qualified \"this\" Data.List as ThisList"+ , "import qualified \"base\" Data.List as BaseList"+ , "useThis = ThisList.x"+ , "useBase = BaseList.map"+ , "wrong1 = ThisList.map"+ , "wrong2 = BaseList.x"+ , "main = pure ()"+ ]+ _ <- createDoc "Data/List.hs" "haskell" thisDataListContent+ _ <- createDoc "Main.hs" "haskell" mainContent+ expectDiagnostics+ [ ( "Main.hs"+ , [(DiagnosticSeverity_Error, (6, 9),+ if ghcVersion >= GHC96 then+ "Variable not in scope: ThisList.map"+ else if ghcVersion >= GHC94 then+ "Variable not in scope: map" -- See https://gitlab.haskell.org/ghc/ghc/-/issues/22130+ else+ "Not in scope: \8216ThisList.map\8217")+ ,(DiagnosticSeverity_Error, (7, 9),+ if ghcVersion >= GHC96 then+ "Variable not in scope: BaseList.x"+ else if ghcVersion >= GHC94 then+ "Variable not in scope: x" -- See https://gitlab.haskell.org/ghc/ghc/-/issues/22130+ else+ "Not in scope: \8216BaseList.x\8217")+ ]+ )+ ]+ , testSessionWait "unqualified warnings" $ do+ let fooContent = T.unlines+ [ "{-# OPTIONS_GHC -Wredundant-constraints #-}"+ , "module Foo where"+ , "foo :: Ord a => a -> Int"+ , "foo _a = 1"+ ]+ _ <- createDoc "Foo.hs" "haskell" fooContent+ expectDiagnostics+ [ ( "Foo.hs"+ -- The test is to make sure that warnings contain unqualified names+ -- where appropriate. The warning should use an unqualified name 'Ord', not+ -- something like 'GHC.Classes.Ord'. The choice of redundant-constraints to+ -- test this is fairly arbitrary.+ , [(DiagnosticSeverity_Warning, (2, if ghcVersion >= GHC94 then 7 else 0), "Redundant constraint: Ord a")+ ]+ )+ ]+ , testSessionWait "lower-case drive" $ do+ let aContent = T.unlines+ [ "module A.A where"+ , "import A.B ()"+ ]+ bContent = T.unlines+ [ "{-# OPTIONS_GHC -Wall #-}"+ , "module A.B where"+ , "import Data.List"+ ]+ uriB <- getDocUri "A/B.hs"+ Just pathB <- pure $ uriToFilePath uriB+ uriB <- pure $+ let (drive, suffix) = splitDrive pathB+ in filePathToUri (joinDrive (lower drive) suffix)+ liftIO $ createDirectoryIfMissing True (takeDirectory pathB)+ liftIO $ writeFileUTF8 pathB $ T.unpack bContent+ uriA <- getDocUri "A/A.hs"+ Just pathA <- pure $ uriToFilePath uriA+ uriA <- pure $+ let (drive, suffix) = splitDrive pathA+ in filePathToUri (joinDrive (lower drive) suffix)+ let itemA = TextDocumentItem uriA "haskell" 0 aContent+ let a = TextDocumentIdentifier uriA+ sendNotification SMethod_TextDocumentDidOpen (DidOpenTextDocumentParams itemA)+ TNotificationMessage{_params = PublishDiagnosticsParams fileUri _ diags} <- skipManyTill anyMessage diagnostic+ -- Check that if we put a lower-case drive in for A.A+ -- the diagnostics for A.B will also be lower-case.+ liftIO $ fileUri @?= uriB+ let msg :: T.Text = (head diags) ^. L.message+ liftIO $ unless ("redundant" `T.isInfixOf` msg) $+ assertFailure ("Expected redundant import but got " <> T.unpack msg)+ closeDoc a+ , testSessionWait "haddock parse error" $ do+ let fooContent = T.unlines+ [ "module Foo where"+ , "foo :: Int"+ , "foo = 1 {-|-}"+ ]+ _ <- createDoc "Foo.hs" "haskell" fooContent+ if ghcVersion >= GHC90 then+ -- Haddock parse errors are ignored on ghc-9.0+ pure ()+ else+ expectDiagnostics+ [ ( "Foo.hs"+ , [(DiagnosticSeverity_Warning, (2, 8), "Haddock parse error on input")]+ )+ ]+ , testSessionWait "strip file path" $ do+ let+ name = "Testing"+ content = T.unlines+ [ "module " <> name <> " where"+ , "value :: Maybe ()"+ , "value = [()]"+ ]+ _ <- createDoc (T.unpack name <> ".hs") "haskell" content+ notification <- skipManyTill anyMessage diagnostic+ let+ offenders =+ L.params .+ L.diagnostics .+ Lens.folded .+ L.message .+ Lens.filtered (T.isInfixOf ("/" <> name <> ".hs:"))+ failure msg = liftIO $ assertFailure $ "Expected file path to be stripped but got " <> T.unpack msg+ Lens.mapMOf_ offenders failure notification+ , testSession' "-Werror in cradle is ignored" $ \sessionDir -> do+ liftIO $ writeFile (sessionDir </> "hie.yaml")+ "cradle: {direct: {arguments: [\"-Wall\", \"-Werror\"]}}"+ let fooContent = T.unlines+ [ "module Foo where"+ , "foo = ()"+ ]+ _ <- createDoc "Foo.hs" "haskell" fooContent+ expectDiagnostics+ [ ( "Foo.hs"+ , [(DiagnosticSeverity_Warning, (1, 0), "Top-level binding with no type signature:")+ ]+ )+ ]+ , testSessionWait "-Werror in pragma is ignored" $ do+ let fooContent = T.unlines+ [ "{-# OPTIONS_GHC -Wall -Werror #-}"+ , "module Foo() where"+ , "foo :: Int"+ , "foo = 1"+ ]+ _ <- createDoc "Foo.hs" "haskell" fooContent+ expectDiagnostics+ [ ( "Foo.hs"+ , [(DiagnosticSeverity_Warning, (3, 0), "Defined but not used:")+ ]+ )+ ]+ , testCase "typecheck-all-parents-of-interest" $ runWithExtraFiles "recomp" $ \dir -> do+ let bPath = dir </> "B.hs"+ pPath = dir </> "P.hs"+ aPath = dir </> "A.hs"++ bSource <- liftIO $ readFileUtf8 bPath -- y :: Int+ pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int+ aSource <- liftIO $ readFileUtf8 aPath -- x = y :: Int++ bdoc <- createDoc bPath "haskell" bSource+ _pdoc <- createDoc pPath "haskell" pSource+ expectDiagnostics+ [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])] -- So that we know P has been loaded++ -- Change y from Int to B which introduces a type error in A (imported from P)+ changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $+ T.unlines ["module B where", "y :: Bool", "y = undefined"]]+ expectDiagnostics+ [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])+ ]++ -- Open A and edit to fix the type error+ adoc <- createDoc aPath "haskell" aSource+ changeDoc adoc [TextDocumentContentChangeEvent . InR . (.==) #text $+ T.unlines ["module A where", "import B", "x :: Bool", "x = y"]]++ expectDiagnostics+ [ ( "P.hs",+ [ (DiagnosticSeverity_Error, (4, 6), "Couldn't match expected type 'Int' with actual type 'Bool'"),+ (DiagnosticSeverity_Warning, (4, 0), "Top-level binding")+ ]+ ),+ ("A.hs", [])+ ]+ expectNoMoreDiagnostics 1++ , testSessionWait "deduplicate missing module diagnostics" $ do+ let fooContent = T.unlines [ "module Foo() where" , "import MissingModule" ]+ doc <- createDoc "Foo.hs" "haskell" fooContent+ expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (1,7), "Could not find module 'MissingModule'")])]++ changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ "module Foo() where" ]+ expectDiagnostics []++ changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines+ [ "module Foo() where" , "import MissingModule" ] ]+ expectDiagnostics [("Foo.hs", [(DiagnosticSeverity_Error, (1,7), "Could not find module 'MissingModule'")])]++ , testGroup "Cancellation"+ [ cancellationTestGroup "edit header" editHeader yesSession noParse noTc+ , cancellationTestGroup "edit import" editImport noSession yesParse noTc+ , cancellationTestGroup "edit body" editBody yesSession yesParse yesTc+ ]+ ]+ where+ editPair x y = let p = Position x y ; p' = Position x (y+2) in+ (TextDocumentContentChangeEvent $ InL $ #range .== Range p p+ .+ #rangeLength .== Nothing+ .+ #text .== "fd"+ ,TextDocumentContentChangeEvent $ InL $ #range .== Range p p'+ .+ #rangeLength .== Nothing+ .+ #text .== "")+ editHeader = editPair 0 0+ editImport = editPair 2 10+ editBody = editPair 3 10++ noParse = False+ yesParse = True++ noSession = False+ yesSession = True++ noTc = False+ yesTc = True++cancellationTestGroup :: TestName -> (TextDocumentContentChangeEvent, TextDocumentContentChangeEvent) -> Bool -> Bool -> Bool -> TestTree+cancellationTestGroup name edits sessionDepsOutcome parseOutcome tcOutcome = testGroup name+ [ cancellationTemplate edits Nothing+ , cancellationTemplate edits $ Just ("GetFileContents", True)+ , cancellationTemplate edits $ Just ("GhcSession", True)+ -- the outcome for GetModSummary is always True because parseModuleHeader never fails (!)+ , cancellationTemplate edits $ Just ("GetModSummary", True)+ , cancellationTemplate edits $ Just ("GetModSummaryWithoutTimestamps", True)+ -- getLocatedImports never fails+ , cancellationTemplate edits $ Just ("GetLocatedImports", True)+ , cancellationTemplate edits $ Just ("GhcSessionDeps", sessionDepsOutcome)+ , cancellationTemplate edits $ Just ("GetParsedModule", parseOutcome)+ , cancellationTemplate edits $ Just ("TypeCheck", tcOutcome)+ , cancellationTemplate edits $ Just ("GetHieAst", tcOutcome)+ ]++cancellationTemplate :: (TextDocumentContentChangeEvent, TextDocumentContentChangeEvent) -> Maybe (String, Bool) -> TestTree+cancellationTemplate (edit, undoEdit) mbKey = testCase (maybe "-" fst mbKey) $ runTestNoKick $ do+ doc <- createDoc "Foo.hs" "haskell" $ T.unlines+ [ "{-# OPTIONS_GHC -Wall #-}"+ , "module Foo where"+ , "import Data.List()"+ , "f0 x = (x,x)"+ ]++ -- for the example above we expect one warning+ let missingSigDiags = [(DiagnosticSeverity_Warning, (3, 0), "Top-level binding") ]+ typeCheck doc >> expectCurrentDiagnostics doc missingSigDiags++ -- Now we edit the document and wait for the given key (if any)+ changeDoc doc [edit]+ whenJust mbKey $ \(key, expectedResult) -> do+ WaitForIdeRuleResult{ideResultSuccess} <- waitForAction key doc+ liftIO $ ideResultSuccess @?= expectedResult++ -- The 2nd edit cancels the active session and unbreaks the file+ -- wait for typecheck and check that the current diagnostics are accurate+ changeDoc doc [undoEdit]+ typeCheck doc >> expectCurrentDiagnostics doc missingSigDiags++ expectNoMoreDiagnostics 0.5+ where+ -- similar to run except it disables kick+ runTestNoKick s = withTempDir $ \dir -> runInDir' dir "." "." ["--test-no-kick"] s++ typeCheck doc = do+ WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc+ liftIO $ assertBool "The file should typecheck" ideResultSuccess+ -- wait for the debouncer to publish diagnostics if the rule runs+ liftIO $ sleep 0.2+ -- flush messages to ensure current diagnostics state is updated+ flushMessages
+ test/exe/ExceptionTests.hs view
@@ -0,0 +1,155 @@++module ExceptionTests (tests) where++import Control.Exception (ArithException (DivideByZero),+ throwIO)+import Control.Lens+import Control.Monad.Error.Class (MonadError (throwError))+import Control.Monad.IO.Class (liftIO)+import qualified Data.Aeson as A+import Data.Text as T+import Development.IDE.Core.Shake (IdeState (..))+import qualified Development.IDE.LSP.Notifications as Notifications+import qualified Development.IDE.Main as IDE+import Development.IDE.Plugin.HLS (toResponseError)+import Development.IDE.Plugin.Test as Test+import Development.IDE.Types.Options+import GHC.Base (coerce)+import Ide.Logger (Logger, Recorder,+ WithPriority, cmapWithPrio)+import Ide.Plugin.Error+import Ide.PluginUtils (idePluginsToPluginDesc,+ pluginDescToIdePlugins)+import Ide.Types+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import LogType (Log (..))+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: Recorder (WithPriority Log) -> Logger -> TestTree+tests recorder logger = do+ testGroup "Exceptions and PluginError" [+ testGroup "Testing that IO Exceptions are caught in..."+ [ testCase "PluginHandlers" $ do+ let pluginId = "plugin-handler-exception"+ plugins = pluginDescToIdePlugins $+ [ (defaultPluginDescriptor pluginId)+ { pluginHandlers = mconcat+ [ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do+ _ <- liftIO $ throwIO DivideByZero+ pure (InL [])+ ]+ }]+ testIde recorder (testingLite recorder logger plugins) $ do+ doc <- createDoc "A.hs" "haskell" "module A where"+ waitForProgressDone+ (view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)+ case lens of+ Left (ResponseError {_code = InR ErrorCodes_InternalError, _message}) ->+ liftIO $ assertBool "We caught an error, but it wasn't ours!"+ (T.isInfixOf "divide by zero" _message && T.isInfixOf (coerce pluginId) _message)+ _ -> liftIO $ assertFailure $ show lens++ , testCase "Commands" $ do+ let pluginId = "command-exception"+ commandId = CommandId "exception"+ plugins = pluginDescToIdePlugins $+ [ (defaultPluginDescriptor pluginId)+ { pluginCommands =+ [ PluginCommand commandId "Causes an exception" $ \_ (_::Int) -> do+ _ <- liftIO $ throwIO DivideByZero+ pure (InR Null)+ ]+ }]+ testIde recorder (testingLite recorder logger plugins) $ do+ _ <- createDoc "A.hs" "haskell" "module A where"+ waitForProgressDone+ let cmd = mkLspCommand (coerce pluginId) commandId "" (Just [A.toJSON (1::Int)])+ execParams = ExecuteCommandParams Nothing (cmd ^. L.command) (cmd ^. L.arguments)+ (view L.result -> res) <- request SMethod_WorkspaceExecuteCommand execParams+ case res of+ Left (ResponseError {_code = InR ErrorCodes_InternalError, _message}) ->+ liftIO $ assertBool "We caught an error, but it wasn't ours!"+ (T.isInfixOf "divide by zero" _message && T.isInfixOf (coerce pluginId) _message)+ _ -> liftIO $ assertFailure $ show res++ , testCase "Notification Handlers" $ do+ let pluginId = "notification-exception"+ plugins = pluginDescToIdePlugins $+ [ (defaultPluginDescriptor pluginId)+ { pluginNotificationHandlers = mconcat+ [ mkPluginNotificationHandler SMethod_TextDocumentDidOpen $ \_ _ _ _ ->+ liftIO $ throwIO DivideByZero+ ]+ , pluginHandlers = mconcat+ [ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do+ pure (InL [])+ ]+ }]+ testIde recorder (testingLite recorder logger plugins) $ do+ doc <- createDoc "A.hs" "haskell" "module A where"+ waitForProgressDone+ (view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)+ case lens of+ Right (InL []) ->+ -- We don't get error responses from notification handlers, so+ -- we can only make sure that the server is still responding+ pure ()+ _ -> liftIO $ assertFailure $ "We should have had an empty list" <> show lens]++ , testGroup "Testing PluginError order..."+ [ pluginOrderTestCase recorder logger "InternalError over InvalidParams" PluginInternalError PluginInvalidParams+ , pluginOrderTestCase recorder logger "InvalidParams over InvalidUserState" PluginInvalidParams PluginInvalidUserState+ , pluginOrderTestCase recorder logger "InvalidUserState over RequestRefused" PluginInvalidUserState PluginRequestRefused+ ]+ ]++testingLite :: Recorder (WithPriority Log) -> Logger -> IdePlugins IdeState -> IDE.Arguments+testingLite recorder logger plugins =+ let+ arguments@IDE.Arguments{ argsIdeOptions } =+ IDE.defaultArguments (cmapWithPrio LogIDEMain recorder) logger plugins+ hlsPlugins = pluginDescToIdePlugins $+ idePluginsToPluginDesc plugins+ ++ [Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core"]+ ++ [Test.blockCommandDescriptor "block-command", Test.plugin]+ ideOptions config sessionLoader =+ let+ defOptions = argsIdeOptions config sessionLoader+ in+ defOptions{ optTesting = IdeTesting True }+ in+ arguments+ { IDE.argsHlsPlugins = hlsPlugins+ , IDE.argsIdeOptions = ideOptions+ }++pluginOrderTestCase :: Recorder (WithPriority Log) -> Logger -> TestName -> (T.Text -> PluginError) -> (T.Text -> PluginError) -> TestTree+pluginOrderTestCase recorder logger msg err1 err2 =+ testCase msg $ do+ let pluginId = "error-order-test"+ plugins = pluginDescToIdePlugins $+ [ (defaultPluginDescriptor pluginId)+ { pluginHandlers = mconcat+ [ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do+ throwError $ err1 "error test"+ ,mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do+ throwError $ err2 "error test"+ ]+ }]+ testIde recorder (testingLite recorder logger plugins) $ do+ doc <- createDoc "A.hs" "haskell" "module A where"+ waitForProgressDone+ (view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)+ case lens of+ Left re | toResponseError (pluginId, err1 "error test") == re -> pure ()+ | otherwise -> liftIO $ assertFailure "We caught an error, but it wasn't ours!"+ _ -> liftIO $ assertFailure $ show lens
+ test/exe/FindDefinitionAndHoverTests.hs view
@@ -0,0 +1,252 @@++{-# LANGUAGE MultiWayIf #-}++module FindDefinitionAndHoverTests (tests) where++import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.Foldable+import Data.Maybe+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.GHC.Util+import Development.IDE.Test (expectDiagnostics,+ standardizeQuotes)+import Development.IDE.Types.Location+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.FilePath+import System.Info.Extra (isWindows)+-- import Test.QuickCheck.Instances ()+import Control.Lens ((^.))+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+import Text.Regex.TDFA ((=~))++tests :: TestTree+tests = let++ tst :: (TextDocumentIdentifier -> Position -> Session a, a -> Session [Expect] -> Session ()) -> Position -> String -> Session [Expect] -> String -> TestTree+ tst (get, check) pos sfp targetRange title = testSessionWithExtraFiles "hover" title $ \dir -> do++ -- Dirty the cache to check that definitions work even in the presence of iface files+ liftIO $ runInDir dir $ do+ let fooPath = dir </> "Foo.hs"+ fooSource <- liftIO $ readFileUtf8 fooPath+ fooDoc <- createDoc fooPath "haskell" fooSource+ _ <- getHover fooDoc $ Position 4 3+ closeDoc fooDoc++ doc <- openTestDataDoc (dir </> sfp)+ waitForProgressDone+ found <- get doc pos+ check found targetRange++++ checkHover :: Maybe Hover -> Session [Expect] -> Session ()+ checkHover hover expectations = traverse_ check =<< expectations where++ check expected =+ case hover of+ Nothing -> unless (expected == ExpectNoHover) $ liftIO $ assertFailure "no hover found"+ Just Hover{_contents = (InL MarkupContent{_value = standardizeQuotes -> msg})+ ,_range = rangeInHover } ->+ case expected of+ ExpectRange expectedRange -> checkHoverRange expectedRange rangeInHover msg+ ExpectHoverRange expectedRange -> checkHoverRange expectedRange rangeInHover msg+ ExpectHoverText snippets -> liftIO $ traverse_ (`assertFoundIn` msg) snippets+ ExpectHoverExcludeText snippets -> liftIO $ traverse_ (`assertNotFoundIn` msg) snippets+ ExpectHoverTextRegex re -> liftIO $ assertBool ("Regex not found in " <> T.unpack msg) (msg =~ re :: Bool)+ ExpectNoHover -> liftIO $ assertFailure $ "Expected no hover but got " <> show hover+ _ -> pure () -- all other expectations not relevant to hover+ _ -> liftIO $ assertFailure $ "test not expecting this kind of hover info" <> show hover++ extractLineColFromHoverMsg :: T.Text -> [T.Text]+ extractLineColFromHoverMsg = T.splitOn ":" . head . T.splitOn "*" . last . T.splitOn (sourceFileName <> ":")++ checkHoverRange :: Range -> Maybe Range -> T.Text -> Session ()+ checkHoverRange expectedRange rangeInHover msg =+ let+ lineCol = extractLineColFromHoverMsg msg+ -- looks like hovers use 1-based numbering while definitions use 0-based+ -- turns out that they are stored 1-based in RealSrcLoc by GHC itself.+ adjust Position{_line = l, _character = c} =+ Position{_line = l + 1, _character = c + 1}+ in+ case map (read . T.unpack) lineCol of+ [l,c] -> liftIO $ adjust (expectedRange ^. L.start) @=? Position l c+ _ -> liftIO $ assertFailure $+ "expected: " <> show ("[...]" <> sourceFileName <> ":<LINE>:<COL>**[...]", Just expectedRange) <>+ "\n but got: " <> show (msg, rangeInHover)++ assertFoundIn :: T.Text -> T.Text -> Assertion+ assertFoundIn part whole = assertBool+ (T.unpack $ "failed to find: `" <> part <> "` in hover message:\n" <> whole)+ (part `T.isInfixOf` whole)++ assertNotFoundIn :: T.Text -> T.Text -> Assertion+ assertNotFoundIn part whole = assertBool+ (T.unpack $ "found unexpected: `" <> part <> "` in hover message:\n" <> whole)+ (not . T.isInfixOf part $ whole)++ sourceFilePath = T.unpack sourceFileName+ sourceFileName = "GotoHover.hs"++ mkFindTests tests = testGroup "get"+ [ testGroup "definition" $ mapMaybe fst tests+ , testGroup "hover" $ mapMaybe snd tests+ , checkFileCompiles sourceFilePath $+ expectDiagnostics+ [ ( "GotoHover.hs", [(DiagnosticSeverity_Error, (62, 7), "Found hole: _")])+ , ( "GotoHover.hs", [(DiagnosticSeverity_Error, (65, 8), "Found hole: _")])+ ]+ , testGroup "type-definition" typeDefinitionTests+ , testGroup "hover-record-dot-syntax" recordDotSyntaxTests ]++ typeDefinitionTests = [ tst (getTypeDefinitions, checkDefs) aaaL14 sourceFilePath (pure tcData) "Saturated data con"+ , tst (getTypeDefinitions, checkDefs) aL20 sourceFilePath (pure [ExpectNoDefinitions]) "Polymorphic variable"]++ recordDotSyntaxTests+ | ghcVersion >= GHC92 =+ [ tst (getHover, checkHover) (Position 19 24) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["x :: MyRecord"]]) "hover over parent"+ , tst (getHover, checkHover) (Position 19 25) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over dot shows child"+ , tst (getHover, checkHover) (Position 19 26) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over child"+ ]+ | otherwise = []++ test runDef runHover look expect = testM runDef runHover look (return expect)++ testM runDef runHover look expect title =+ ( runDef $ tst def look sourceFilePath expect title+ , runHover $ tst hover look sourceFilePath expect title ) where+ def = (getDefinitions, checkDefs)+ hover = (getHover , checkHover)++ -- search locations expectations on results+ fffL4 = fffR ^. L.start; fffR = mkRange 8 4 8 7 ; fff = [ExpectRange fffR]+ fffL8 = Position 12 4 ;+ fffL14 = Position 18 7 ;+ aL20 = Position 19 15+ aaaL14 = Position 18 20 ; aaa = [mkR 11 0 11 3]+ dcL7 = Position 11 11 ; tcDC = [mkR 7 23 9 16]+ dcL12 = Position 16 11 ;+ xtcL5 = Position 9 11 ; xtc = [ExpectExternFail, ExpectHoverText ["Int", "Defined in ", "GHC.Types", "ghc-prim"]]+ tcL6 = Position 10 11 ; tcData = [mkR 7 0 9 16, ExpectHoverText ["TypeConstructor", "GotoHover.hs:8:1"]]+ vvL16 = Position 20 12 ; vv = [mkR 20 4 20 6]+ opL16 = Position 20 15 ; op = [mkR 21 2 21 4]+ opL18 = Position 22 22 ; opp = [mkR 22 13 22 17]+ aL18 = Position 22 20 ; apmp = [mkR 22 10 22 11]+ b'L19 = Position 23 13 ; bp = [mkR 23 6 23 7]+ xvL20 = Position 24 8 ; xvMsg = [ExpectExternFail, ExpectHoverText ["pack", ":: String -> Text", "Data.Text", "text"]]+ clL23 = Position 27 11 ; cls = [mkR 25 0 26 20, ExpectHoverText ["MyClass", "GotoHover.hs:26:1"]]+ clL25 = Position 29 9+ eclL15 = Position 19 8 ; ecls = [ExpectExternFail, ExpectHoverText ["Num", "Defined in ", "GHC.Num", "base"]]+ dnbL29 = Position 33 18 ; dnb = [ExpectHoverText [":: ()"], mkR 33 12 33 21]+ dnbL30 = Position 34 23+ lcbL33 = Position 37 26 ; lcb = [ExpectHoverText [":: Char"], mkR 37 26 37 27]+ lclL33 = Position 37 22+ mclL36 = Position 40 1 ; mcl = [mkR 40 0 40 14]+ mclL37 = Position 41 1+ spaceL37 = Position 41 24 ; space = [ExpectNoDefinitions, ExpectHoverText [":: Char"]]+ docL41 = Position 45 1 ; doc = [ExpectHoverText ["Recognizable docs: kpqz"]]+ ; constr = [ExpectHoverText ["Monad m"]]+ eitL40 = Position 44 28 ; kindE = [ExpectHoverText [if ghcVersion >= GHC92 then ":: Type -> Type -> Type\n" else ":: * -> * -> *\n"]]+ intL40 = Position 44 34 ; kindI = [ExpectHoverText [if ghcVersion >= GHC92 then ":: Type\n" else ":: *\n"]]+ tvrL40 = Position 44 37 ; kindV = [ExpectHoverText [":: * -> *\n"]]+ intL41 = Position 45 20 ; litI = [ExpectHoverText ["7518"]]+ chrL36 = Position 41 24 ; litC = [ExpectHoverText ["'f'"]]+ txtL8 = Position 12 14 ; litT = [ExpectHoverText ["\"dfgy\""]]+ lstL43 = Position 47 12 ; litL = [ExpectHoverText ["[8391 :: Int, 6268]"]]+ outL45 = Position 49 3 ; outSig = [ExpectHoverText ["outer", "Bool"], mkR 50 0 50 5]+ innL48 = Position 52 5 ; innSig = [ExpectHoverText ["inner", "Char"], mkR 49 2 49 7]+ holeL60 = Position 62 7 ; hleInfo = [ExpectHoverText ["_ ::"]]+ holeL65 = Position 65 8 ; hleInfo2 = [ExpectHoverText ["_ :: a -> Maybe a"]]+ cccL17 = Position 17 16 ; docLink = [ExpectHoverTextRegex "\\*Defined in 'GHC.Types'\\* \\*\\(ghc-prim-[0-9.]+\\)\\*\n\n"]+ imported = Position 56 13 ; importedSig = getDocUri "Foo.hs" >>= \foo -> return [ExpectHoverText ["foo", "Foo", "Haddock"], mkL foo 5 0 5 3]+ reexported = Position 55 14 ; reexportedSig = getDocUri "Bar.hs" >>= \bar -> return [ExpectHoverText ["Bar", "Bar", "Haddock"], mkL bar 3 (if ghcVersion >= GHC94 then 5 else 0) 3 (if ghcVersion >= GHC94 then 8 else 14)]+ thLocL57 = Position 59 10 ; thLoc = [ExpectHoverText ["Identity"]]+ cmtL68 = Position 67 0 ; lackOfdEq = [ExpectHoverExcludeText ["$dEq"]]+ import310 = Position 3 10; pkgTxt = [ExpectHoverText ["Data.Text\n\ntext-"]]+ in+ mkFindTests+ -- def hover look expect+ [+ if ghcVersion >= GHC90 then+ -- It suggests either going to the constructor or to the field+ test broken yes fffL4 fff "field in record definition"+ else+ test yes yes fffL4 fff "field in record definition"+ , test yes yes fffL8 fff "field in record construction #1102"+ , test yes yes fffL14 fff "field name used as accessor" -- https://github.com/haskell/ghcide/pull/120 in Calculate.hs+ , test yes yes aaaL14 aaa "top-level name" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes dcL7 tcDC "data constructor record #1029"+ , test yes yes dcL12 tcDC "data constructor plain" -- https://github.com/haskell/ghcide/pull/121+ , test yes yes tcL6 tcData "type constructor #1028" -- https://github.com/haskell/ghcide/pull/147+ , test broken yes xtcL5 xtc "type constructor external #717,1028"+ , test broken yes xvL20 xvMsg "value external package #717" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes vvL16 vv "plain parameter" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes aL18 apmp "pattern match name" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes opL16 op "top-level operator #713" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes opL18 opp "parameter operator" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes b'L19 bp "name in backticks" -- https://github.com/haskell/ghcide/pull/120+ , test yes yes clL23 cls "class in instance declaration #1027"+ , test yes yes clL25 cls "class in signature #1027" -- https://github.com/haskell/ghcide/pull/147+ , test broken yes eclL15 ecls "external class in signature #717,1027"+ , test yes yes dnbL29 dnb "do-notation bind #1073"+ , test yes yes dnbL30 dnb "do-notation lookup"+ , test yes yes lcbL33 lcb "listcomp bind #1073"+ , test yes yes lclL33 lcb "listcomp lookup"+ , test yes yes mclL36 mcl "top-level fn 1st clause"+ , test yes yes mclL37 mcl "top-level fn 2nd clause #1030"+ , if ghcVersion >= GHC810 then+ test yes yes spaceL37 space "top-level fn on space #1002"+ else+ test yes broken spaceL37 space "top-level fn on space #1002"+ , test no yes docL41 doc "documentation #1129"+ , test no yes eitL40 kindE "kind of Either #1017"+ , test no yes intL40 kindI "kind of Int #1017"+ , test no broken tvrL40 kindV "kind of (* -> *) type variable #1017"+ , test no broken intL41 litI "literal Int in hover info #1016"+ , test no broken chrL36 litC "literal Char in hover info #1016"+ , test no broken txtL8 litT "literal Text in hover info #1016"+ , test no broken lstL43 litL "literal List in hover info #1016"+ , test yes yes cmtL68 lackOfdEq "no Core symbols #3280"+ , if ghcVersion >= GHC90 then+ test no yes docL41 constr "type constraint in hover info #1012"+ else+ test no broken docL41 constr "type constraint in hover info #1012"+ , test no yes outL45 outSig "top-level signature #767"+ , test broken broken innL48 innSig "inner signature #767"+ , test no yes holeL60 hleInfo "hole without internal name #831"+ , test no yes holeL65 hleInfo2 "hole with variable"+ , test no yes cccL17 docLink "Haddock html links"+ , testM yes yes imported importedSig "Imported symbol"+ , if | isWindows ->+ -- Flaky on Windows: https://github.com/haskell/haskell-language-server/issues/2997+ testM no yes reexported reexportedSig "Imported symbol (reexported)"+ | otherwise ->+ testM yes yes reexported reexportedSig "Imported symbol (reexported)"+ , if | ghcVersion == GHC90 && isWindows ->+ test no broken thLocL57 thLoc "TH Splice Hover"+ | otherwise ->+ test no yes thLocL57 thLoc "TH Splice Hover"+ , test yes yes import310 pkgTxt "show package name and its version"+ ]+ where yes, broken :: (TestTree -> Maybe TestTree)+ yes = Just -- test should run and pass+ broken = Just . (`xfail` "known broken")+ no = const Nothing -- don't run this test at all+ skip = const Nothing -- unreliable, don't run++checkFileCompiles :: FilePath -> Session () -> TestTree+checkFileCompiles fp diag =+ testSessionWithExtraFiles "hover" ("Does " ++ fp ++ " compile") $ \dir -> do+ void (openTestDataDoc (dir </> fp))+ diag
test/exe/FuzzySearch.hs view
@@ -1,6 +1,5 @@ module FuzzySearch (tests) where -import Control.Monad (guard) import Data.Char (toLower) import Data.Maybe (catMaybes) import qualified Data.Monoid.Textual as T@@ -8,12 +7,10 @@ import qualified Data.Text as Text import Prelude hiding (filter) import System.Directory (doesFileExist)-import System.Info.Extra (isWindows) import System.IO.Unsafe (unsafePerformIO) import Test.QuickCheck import Test.Tasty import Test.Tasty.ExpectedFailure-import Test.Tasty.HUnit import Test.Tasty.QuickCheck (testProperty) import qualified Text.Fuzzy as Fuzzy import Text.Fuzzy (Fuzzy (..))
+ test/exe/GarbageCollectionTests.hs view
@@ -0,0 +1,94 @@++{-# LANGUAGE OverloadedLabels #-}++module GarbageCollectionTests (tests) where++import Control.Monad.IO.Class (liftIO)+import Data.Row+import qualified Data.Set as Set+import qualified Data.Text as T+import Development.IDE.Test (expectCurrentDiagnostics,+ getStoredKeys, waitForGC,+ waitForTypecheck)+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+import System.FilePath+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+import Text.Printf (printf)++tests :: TestTree+tests = testGroup "garbage collection"+ [ testGroup "dirty keys"+ [ testSession' "are collected" $ \dir -> do+ liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A]}}"+ doc <- generateGarbage "A" dir+ closeDoc doc+ garbage <- waitForGC+ liftIO $ assertBool "no garbage was found" $ not $ null garbage++ , testSession' "are deleted from the state" $ \dir -> do+ liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A]}}"+ docA <- generateGarbage "A" dir+ keys0 <- getStoredKeys+ closeDoc docA+ garbage <- waitForGC+ liftIO $ assertBool "something is wrong with this test - no garbage found" $ not $ null garbage+ keys1 <- getStoredKeys+ liftIO $ assertBool "keys were not deleted from the state" (length keys1 < length keys0)++ , testSession' "are not regenerated unless needed" $ \dir -> do+ liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A.hs, B.hs]}}"+ docA <- generateGarbage "A" dir+ _docB <- generateGarbage "B" dir++ -- garbage collect A keys+ keysBeforeGC <- getStoredKeys+ closeDoc docA+ garbage <- waitForGC+ liftIO $ assertBool "something is wrong with this test - no garbage found" $ not $ null garbage+ keysAfterGC <- getStoredKeys+ liftIO $ assertBool "something is wrong with this test - keys were not deleted from the state"+ (length keysAfterGC < length keysBeforeGC)++ -- re-typecheck B and check that the keys for A have not materialized back+ _docB <- generateGarbage "B" dir+ keysB <- getStoredKeys+ let regeneratedKeys = Set.filter (not . isExpected) $+ Set.intersection (Set.fromList garbage) (Set.fromList keysB)+ liftIO $ regeneratedKeys @?= mempty++ , testSession' "regenerate successfully" $ \dir -> do+ liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A]}}"+ docA <- generateGarbage "A" dir+ closeDoc docA+ garbage <- waitForGC+ liftIO $ assertBool "no garbage was found" $ not $ null garbage+ let edit = T.unlines+ [ "module A where"+ , "a :: Bool"+ , "a = ()"+ ]+ doc <- generateGarbage "A" dir+ changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ edit]+ builds <- waitForTypecheck doc+ liftIO $ assertBool "it still builds" builds+ expectCurrentDiagnostics doc [(DiagnosticSeverity_Error, (2,4), "Couldn't match expected type")]+ ]+ ]+ where+ isExpected k = any (`T.isPrefixOf` k) ["GhcSessionIO"]++ generateGarbage :: String -> FilePath -> Session TextDocumentIdentifier+ generateGarbage modName dir = do+ let fp = modName <> ".hs"+ body = printf "module %s where" modName+ doc <- createDoc fp "haskell" (T.pack body)+ liftIO $ writeFile (dir </> fp) body+ builds <- waitForTypecheck doc+ liftIO $ assertBool "something is wrong with this test" builds+ return doc
+ test/exe/HaddockTests.hs view
@@ -0,0 +1,90 @@++module HaddockTests (tests) where++import Development.IDE.Spans.Common+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests+ = testGroup "haddock"+ [ testCase "Num" $ checkHaddock+ (unlines+ [ "However, '(+)' and '(*)' are"+ , "customarily expected to define a ring and have the following properties:"+ , ""+ , "[__Associativity of (+)__]: @(x + y) + z@ = @x + (y + z)@"+ , "[__Commutativity of (+)__]: @x + y@ = @y + x@"+ , "[__@fromInteger 0@ is the additive identity__]: @x + fromInteger 0@ = @x@"+ ]+ )+ (unlines+ [ ""+ , ""+ , "However, `(+)` and `(*)` are"+ , "customarily expected to define a ring and have the following properties: "+ , "+ ****Associativity of (+)****: `(x + y) + z` = `x + (y + z)`"+ , "+ ****Commutativity of (+)****: `x + y` = `y + x`"+ , "+ ****`fromInteger 0` is the additive identity****: `x + fromInteger 0` = `x`"+ ]+ )+ , testCase "unsafePerformIO" $ checkHaddock+ (unlines+ [ "may require"+ , "different precautions:"+ , ""+ , " * Use @{\\-\\# NOINLINE foo \\#-\\}@ as a pragma on any function @foo@"+ , " that calls 'unsafePerformIO'. If the call is inlined,"+ , " the I\\/O may be performed more than once."+ , ""+ , " * Use the compiler flag @-fno-cse@ to prevent common sub-expression"+ , " elimination being performed on the module."+ , ""+ ]+ )+ (unlines+ [ ""+ , ""+ , "may require"+ , "different precautions: "+ , "+ Use `{-# NOINLINE foo #-}` as a pragma on any function `foo` "+ , " that calls `unsafePerformIO` . If the call is inlined,"+ , " the I/O may be performed more than once."+ , ""+ , "+ Use the compiler flag `-fno-cse` to prevent common sub-expression"+ , " elimination being performed on the module."+ , ""+ ]+ )+ , testCase "ordered list" $ checkHaddock+ (unlines+ [ "may require"+ , "different precautions:"+ , ""+ , " 1. Use @{\\-\\# NOINLINE foo \\#-\\}@ as a pragma on any function @foo@"+ , " that calls 'unsafePerformIO'. If the call is inlined,"+ , " the I\\/O may be performed more than once."+ , ""+ , " 2. Use the compiler flag @-fno-cse@ to prevent common sub-expression"+ , " elimination being performed on the module."+ , ""+ ]+ )+ (unlines+ [ ""+ , ""+ , "may require"+ , "different precautions: "+ , "1. Use `{-# NOINLINE foo #-}` as a pragma on any function `foo` "+ , " that calls `unsafePerformIO` . If the call is inlined,"+ , " the I/O may be performed more than once."+ , ""+ , "2. Use the compiler flag `-fno-cse` to prevent common sub-expression"+ , " elimination being performed on the module."+ , ""+ ]+ )+ ]+ where+ checkHaddock s txt = spanDocToMarkdownForTest s @?= txt
test/exe/HieDbRetry.hs view
@@ -1,22 +1,21 @@ {-# LANGUAGE MultiWayIf #-} module HieDbRetry (tests) where -import Control.Concurrent.Extra (Var, modifyVar, newVar, readVar,- withVar)-import Control.Exception (ErrorCall (ErrorCall), evaluate,- throwIO, tryJust)-import Control.Monad.IO.Class (MonadIO (liftIO))-import Data.Tuple.Extra (dupe)-import qualified Database.SQLite.Simple as SQLite-import Development.IDE.Session (retryOnException,- retryOnSqliteBusy)-import qualified Development.IDE.Session as Session-import Development.IDE.Types.Logger (Recorder (Recorder, logger_),- WithPriority (WithPriority, payload),- cmapWithPrio)-import qualified System.Random as Random-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (assertFailure, testCase, (@?=))+import Control.Concurrent.Extra (Var, modifyVar, newVar, readVar,+ withVar)+import Control.Exception (ErrorCall (ErrorCall), evaluate,+ throwIO, tryJust)+import Control.Monad.IO.Class (MonadIO (liftIO))+import Data.Tuple.Extra (dupe)+import qualified Database.SQLite.Simple as SQLite+import Development.IDE.Session (retryOnException, retryOnSqliteBusy)+import qualified Development.IDE.Session as Session+import Ide.Logger (Recorder (Recorder, logger_),+ WithPriority (WithPriority, payload),+ cmapWithPrio)+import qualified System.Random as Random+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertFailure, testCase, (@?=)) data Log = LogSession Session.Log
+ test/exe/HighlightTests.hs view
@@ -0,0 +1,85 @@++module HighlightTests (tests) where++import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.Types.Location+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = testGroup "highlight"+ [ testSessionWait "value" $ do+ doc <- createDoc "A.hs" "haskell" source+ _ <- waitForDiagnostics+ highlights <- getHighlights doc (Position 3 2)+ liftIO $ highlights @?=+ [ DocumentHighlight (R 2 0 2 3) (Just DocumentHighlightKind_Read)+ , DocumentHighlight (R 3 0 3 3) (Just DocumentHighlightKind_Write)+ , DocumentHighlight (R 4 6 4 9) (Just DocumentHighlightKind_Read)+ , DocumentHighlight (R 5 22 5 25) (Just DocumentHighlightKind_Read)+ ]+ , testSessionWait "type" $ do+ doc <- createDoc "A.hs" "haskell" source+ _ <- waitForDiagnostics+ highlights <- getHighlights doc (Position 2 8)+ liftIO $ highlights @?=+ [ DocumentHighlight (R 2 7 2 10) (Just DocumentHighlightKind_Read)+ , DocumentHighlight (R 3 11 3 14) (Just DocumentHighlightKind_Read)+ ]+ , testSessionWait "local" $ do+ doc <- createDoc "A.hs" "haskell" source+ _ <- waitForDiagnostics+ highlights <- getHighlights doc (Position 6 5)+ liftIO $ highlights @?=+ [ DocumentHighlight (R 6 4 6 7) (Just DocumentHighlightKind_Write)+ , DocumentHighlight (R 6 10 6 13) (Just DocumentHighlightKind_Read)+ , DocumentHighlight (R 7 12 7 15) (Just DocumentHighlightKind_Read)+ ]+ , knownBrokenForGhcVersions [GHC90, GHC92, GHC94, GHC96] "Ghc9 highlights the constructor and not just this field" $+ testSessionWait "record" $ do+ doc <- createDoc "A.hs" "haskell" recsource+ _ <- waitForDiagnostics+ highlights <- getHighlights doc (Position 4 15)+ liftIO $ highlights @?=+ -- Span is just the .. on 8.10, but Rec{..} before+ [ if ghcVersion >= GHC810+ then DocumentHighlight (R 4 8 4 10) (Just DocumentHighlightKind_Write)+ else DocumentHighlight (R 4 4 4 11) (Just DocumentHighlightKind_Write)+ , DocumentHighlight (R 4 14 4 20) (Just DocumentHighlightKind_Read)+ ]+ highlights <- getHighlights doc (Position 3 17)+ liftIO $ highlights @?=+ [ DocumentHighlight (R 3 17 3 23) (Just DocumentHighlightKind_Write)+ -- Span is just the .. on 8.10, but Rec{..} before+ , if ghcVersion >= GHC810+ then DocumentHighlight (R 4 8 4 10) (Just DocumentHighlightKind_Read)+ else DocumentHighlight (R 4 4 4 11) (Just DocumentHighlightKind_Read)+ ]+ ]+ where+ source = T.unlines+ ["{-# OPTIONS_GHC -Wunused-binds #-}"+ ,"module Highlight () where"+ ,"foo :: Int"+ ,"foo = 3 :: Int"+ ,"bar = foo"+ ," where baz = let x = foo in x"+ ,"baz arg = arg + x"+ ," where x = arg"+ ]+ recsource = T.unlines+ ["{-# LANGUAGE RecordWildCards #-}"+ ,"{-# OPTIONS_GHC -Wunused-binds #-}"+ ,"module Highlight () where"+ ,"data Rec = Rec { field1 :: Int, field2 :: Char }"+ ,"foo Rec{..} = field2 + field1"+ ]
+ test/exe/IfaceTests.hs view
@@ -0,0 +1,163 @@++{-# LANGUAGE OverloadedLabels #-}++module IfaceTests (tests) where++import Control.Monad.IO.Class (liftIO)+import Data.Row+import qualified Data.Text as T+import Development.IDE.GHC.Util+import Development.IDE.Test (configureCheckProject,+ expectDiagnostics,+ expectNoMoreDiagnostics,+ getInterfaceFilesDir)+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.Directory+import System.FilePath+import System.IO.Extra hiding (withTempDir)+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = testGroup "Interface loading tests"+ [ -- https://github.com/haskell/ghcide/pull/645/+ ifaceErrorTest+ , ifaceErrorTest2+ , ifaceErrorTest3+ , ifaceTHTest+ ]+++-- | test that TH reevaluates across interfaces+ifaceTHTest :: TestTree+ifaceTHTest = testCase "iface-th-test" $ runWithExtraFiles "TH" $ \dir -> do+ let aPath = dir </> "THA.hs"+ bPath = dir </> "THB.hs"+ cPath = dir </> "THC.hs"++ aSource <- liftIO $ readFileUtf8 aPath -- [TH] a :: ()+ _bSource <- liftIO $ readFileUtf8 bPath -- a :: ()+ cSource <- liftIO $ readFileUtf8 cPath -- c = a :: ()++ cdoc <- createDoc cPath "haskell" cSource++ -- Change [TH]a from () to Bool+ liftIO $ writeFileUTF8 aPath (unlines $ init (lines $ T.unpack aSource) ++ ["th_a = [d| a = False|]"])++ -- Check that the change propagates to C+ changeDoc cdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ cSource]+ expectDiagnostics+ [("THC.hs", [(DiagnosticSeverity_Error, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'")])+ ,("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")])]+ closeDoc cdoc++ifaceErrorTest :: TestTree+ifaceErrorTest = testCase "iface-error-test-1" $ runWithExtraFiles "recomp" $ \dir -> do+ configureCheckProject True+ let bPath = dir </> "B.hs"+ pPath = dir </> "P.hs"++ bSource <- liftIO $ readFileUtf8 bPath -- y :: Int+ pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int++ bdoc <- createDoc bPath "haskell" bSource+ expectDiagnostics+ [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])] -- So what we know P has been loaded++ -- Change y from Int to B+ changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines ["module B where", "y :: Bool", "y = undefined"]]+ -- save so that we can that the error propagates to A+ sendNotification SMethod_TextDocumentDidSave (DidSaveTextDocumentParams bdoc Nothing)+++ -- Check that the error propagates to A+ expectDiagnostics+ [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])]++ -- Check that we wrote the interfaces for B when we saved+ hidir <- getInterfaceFilesDir bdoc+ hi_exists <- liftIO $ doesFileExist $ hidir </> "B.hi"+ liftIO $ assertBool ("Couldn't find B.hi in " ++ hidir) hi_exists++ pdoc <- openDoc pPath "haskell"+ expectDiagnostics+ [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])+ ]+ changeDoc pdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ pSource <> "\nfoo = y :: Bool" ]+ -- Now in P we have+ -- bar = x :: Int+ -- foo = y :: Bool+ -- HOWEVER, in A...+ -- x = y :: Int+ -- This is clearly inconsistent, and the expected outcome a bit surprising:+ -- - The diagnostic for A has already been received. Ghcide does not repeat diagnostics+ -- - P is being typechecked with the last successful artifacts for A.+ expectDiagnostics+ [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])+ ,("P.hs", [(DiagnosticSeverity_Warning,(6,0), "Top-level binding")])+ ]+ expectNoMoreDiagnostics 2++ifaceErrorTest2 :: TestTree+ifaceErrorTest2 = testCase "iface-error-test-2" $ runWithExtraFiles "recomp" $ \dir -> do+ let bPath = dir </> "B.hs"+ pPath = dir </> "P.hs"++ bSource <- liftIO $ readFileUtf8 bPath -- y :: Int+ pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int++ bdoc <- createDoc bPath "haskell" bSource+ pdoc <- createDoc pPath "haskell" pSource+ expectDiagnostics+ [("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])] -- So that we know P has been loaded++ -- Change y from Int to B+ changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines ["module B where", "y :: Bool", "y = undefined"]]++ -- Add a new definition to P+ changeDoc pdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ pSource <> "\nfoo = y :: Bool" ]+ -- Now in P we have+ -- bar = x :: Int+ -- foo = y :: Bool+ -- HOWEVER, in A...+ -- x = y :: Int+ expectDiagnostics+ -- As in the other test, P is being typechecked with the last successful artifacts for A+ -- (ot thanks to -fdeferred-type-errors)+ [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])+ ,("P.hs", [(DiagnosticSeverity_Warning, (4, 0), "Top-level binding")])+ ,("P.hs", [(DiagnosticSeverity_Warning, (6, 0), "Top-level binding")])+ ]++ expectNoMoreDiagnostics 2++ifaceErrorTest3 :: TestTree+ifaceErrorTest3 = testCase "iface-error-test-3" $ runWithExtraFiles "recomp" $ \dir -> do+ let bPath = dir </> "B.hs"+ pPath = dir </> "P.hs"++ bSource <- liftIO $ readFileUtf8 bPath -- y :: Int+ pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int++ bdoc <- createDoc bPath "haskell" bSource++ -- Change y from Int to B+ changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ T.unlines ["module B where", "y :: Bool", "y = undefined"]]++ -- P should not typecheck, as there are no last valid artifacts for A+ _pdoc <- createDoc pPath "haskell" pSource++ -- In this example the interface file for A should not exist (modulo the cache folder)+ -- Despite that P still type checks, as we can generate an interface file for A thanks to -fdeferred-type-errors+ expectDiagnostics+ [("A.hs", [(DiagnosticSeverity_Error, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])+ ,("P.hs", [(DiagnosticSeverity_Warning,(4,0), "Top-level binding")])+ ]+ expectNoMoreDiagnostics 2
+ test/exe/InitializeResponseTests.hs view
@@ -0,0 +1,97 @@++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-}++module InitializeResponseTests (tests) where++import Control.Monad+import Data.List.Extra+import Data.Row+import qualified Data.Text as T+import Development.IDE.Plugin.TypeLenses (typeLensCommandId)+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+-- import Test.QuickCheck.Instances ()+import Control.Lens ((^.))+import Development.IDE.Plugin.Test (blockCommandId)+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = withResource acquire release tests where++ -- these tests document and monitor the evolution of the+ -- capabilities announced by the server in the initialize+ -- response. Currently the server advertises almost no capabilities+ -- at all, in some cases failing to announce capabilities that it+ -- actually does provide! Hopefully this will change ...+ tests :: IO (TResponseMessage Method_Initialize) -> TestTree+ tests getInitializeResponse =+ testGroup "initialize response capabilities"+ [ chk " text doc sync" _textDocumentSync tds+ , chk " hover" _hoverProvider (Just $ InL True)+ , chk " completion" _completionProvider (Just $ CompletionOptions Nothing (Just ["."]) Nothing (Just True) Nothing)+ , chk "NO signature help" _signatureHelpProvider Nothing+ , chk " goto definition" _definitionProvider (Just $ InL True)+ , chk " goto type definition" _typeDefinitionProvider (Just $ InL True)+ -- BUG in lsp-test, this test fails, just change the accepted response+ -- for now+ , chk "NO goto implementation" _implementationProvider (Just $ InL False)+ , chk " find references" _referencesProvider (Just $ InL True)+ , chk " doc highlight" _documentHighlightProvider (Just $ InL True)+ , chk " doc symbol" _documentSymbolProvider (Just $ InL True)+ , chk " workspace symbol" _workspaceSymbolProvider (Just $ InL True)+ , chk " code action" _codeActionProvider (Just $ InL False)+ , chk " code lens" _codeLensProvider (Just $ CodeLensOptions (Just False) (Just False))+ , chk "NO doc formatting" _documentFormattingProvider (Just $ InL False)+ , chk "NO doc range formatting"+ _documentRangeFormattingProvider (Just $ InL False)+ , chk "NO doc formatting on typing"+ _documentOnTypeFormattingProvider Nothing+ , chk "NO renaming" _renameProvider (Just $ InL False)+ , chk "NO doc link" _documentLinkProvider Nothing+ , chk "NO color" (^. L.colorProvider) (Just $ InL False)+ , chk "NO folding range" _foldingRangeProvider (Just $ InL False)+ , che " execute command" _executeCommandProvider [typeLensCommandId, blockCommandId]+ , chk " workspace" (^. L.workspace) (Just $ #workspaceFolders .== Just WorkspaceFoldersServerCapabilities{_supported = Just True, _changeNotifications = Just ( InR True )}+ .+ #fileOperations .== Nothing)+ , chk "NO experimental" (^. L.experimental) Nothing+ ] where++ tds = Just (InL (TextDocumentSyncOptions+ { _openClose = Just True+ , _change = Just TextDocumentSyncKind_Incremental+ , _willSave = Nothing+ , _willSaveWaitUntil = Nothing+ , _save = Just (InR $ SaveOptions {_includeText = Nothing})}))++ chk :: (Eq a, Show a) => TestName -> (ServerCapabilities -> a) -> a -> TestTree+ chk title getActual expected =+ testCase title $ getInitializeResponse >>= \ir -> expected @=? (getActual . innerCaps) ir++ che :: TestName -> (ServerCapabilities -> Maybe ExecuteCommandOptions) -> [T.Text] -> TestTree+ che title getActual expected = testCase title doTest+ where+ doTest = do+ ir <- getInitializeResponse+ let Just ExecuteCommandOptions {_commands = commands} = getActual $ innerCaps ir+ commandNames = (!! 2) . T.splitOn ":" <$> commands+ zipWithM_ (\e o -> T.isSuffixOf e o @? show (e,o)) (sort expected) (sort commandNames)++ innerCaps :: TResponseMessage Method_Initialize -> ServerCapabilities+ innerCaps (TResponseMessage _ _ (Right (InitializeResult c _))) = c+ innerCaps (TResponseMessage _ _ (Left _)) = error "Initialization error"++ acquire :: IO (TResponseMessage Method_Initialize)+ acquire = run initializeResponse++ release :: TResponseMessage Method_Initialize -> IO ()+ release = const $ pure ()+
+ test/exe/LogType.hs view
@@ -0,0 +1,21 @@+module LogType (Log(..)) where++import qualified Development.IDE.LSP.Notifications as Notifications+import qualified Development.IDE.Main as IDE+import qualified Development.IDE.Plugin.HLS.GhcIde as Ghcide++import Ide.Logger (Pretty (pretty))+import Language.LSP.VFS (VfsLog)++data Log+ = LogGhcIde Ghcide.Log+ | LogIDEMain IDE.Log+ | LogVfs VfsLog+ | LogNotifications Notifications.Log++instance Pretty Log where+ pretty = \case+ LogGhcIde log -> pretty log+ LogIDEMain log -> pretty log+ LogVfs log -> pretty log+ LogNotifications log -> pretty log
test/exe/Main.hs view
@@ -26,3603 +26,102 @@ Therefore, avoid mixing both progress reports and diagnostics in the same test -} -{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE ImplicitParams #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE TypeOperators #-}-{-# OPTIONS_GHC -Wno-deprecations -Wno-unticked-promoted-constructors #-}--module Main (main) where--import Control.Applicative.Combinators-import Control.Concurrent-import Control.Exception (bracket_, catch,- finally)-import qualified Control.Lens as Lens-import Control.Monad-import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Aeson (toJSON)-import qualified Data.Aeson as A-import Data.Default-import Data.Foldable-import Data.List.Extra-import Data.Maybe-import qualified Data.Set as Set-import qualified Data.Text as T-import Data.Text.Utf16.Rope (Rope)-import qualified Data.Text.Utf16.Rope as Rope-import Development.IDE.Core.PositionMapping (PositionResult (..),- fromCurrent,- positionResultToMaybe,- toCurrent)-import Development.IDE.GHC.Compat (GhcVersion (..),- ghcVersion)-import Development.IDE.GHC.Util-import qualified Development.IDE.Main as IDE-import Development.IDE.Plugin.TypeLenses (typeLensCommandId)-import Development.IDE.Spans.Common-import Development.IDE.Test (Cursor,- canonicalizeUri,- configureCheckProject,- diagnostic,- expectCurrentDiagnostics,- expectDiagnostics,- expectDiagnosticsWithTags,- expectNoMoreDiagnostics,- flushMessages,- getInterfaceFilesDir,- getStoredKeys,- isReferenceReady,- referenceReady,- standardizeQuotes,- waitForAction,- waitForGC,- waitForTypecheck)-import Development.IDE.Test.Runfiles-import qualified Development.IDE.Types.Diagnostics as Diagnostics-import Development.IDE.Types.Location-import Development.Shake (getDirectoryFilesIO)-import Ide.Plugin.Config-import Language.LSP.Test-import Language.LSP.Types hiding- (SemanticTokenAbsolute (length, line),- SemanticTokenRelative (length),- SemanticTokensEdit (_start),- mkRange)-import Language.LSP.Types.Capabilities-import qualified Language.LSP.Types.Lens as Lens (label)-import qualified Language.LSP.Types.Lens as Lsp (diagnostics,- message,- params)-import Language.LSP.VFS (VfsLog, applyChange)-import Network.URI-import System.Directory-import System.Environment.Blank (getEnv, setEnv,- unsetEnv)-import System.Exit (ExitCode (ExitSuccess))-import System.FilePath-import System.Info.Extra (isMac, isWindows)-import qualified System.IO.Extra-import System.IO.Extra hiding (withTempDir)-import System.Mem (performGC)-import System.Process.Extra (CreateProcess (cwd),- createPipe, proc,- readCreateProcessWithExitCode)-import Test.QuickCheck--- import Test.QuickCheck.Instances ()-import Control.Concurrent.Async-import Control.Lens (to, (.~), (^.))-import Control.Monad.Extra (whenJust)-import Data.Function ((&))-import Data.Functor.Identity (runIdentity)-import Data.IORef-import Data.IORef.Extra (atomicModifyIORef_)-import Data.String (IsString (fromString))-import Data.Tuple.Extra-import Development.IDE.Core.FileStore (getModTime)-import qualified Development.IDE.Plugin.HLS.GhcIde as Ghcide-import Development.IDE.Plugin.Test (TestRequest (BlockSeconds),- WaitForIdeRuleResult (..),- blockCommandId)-import Development.IDE.Types.Logger (Logger (Logger),- LoggingColumn (DataColumn, PriorityColumn),- Pretty (pretty),- Priority (Debug),- Recorder (Recorder, logger_),- WithPriority (WithPriority, priority),- cfilter,- cmapWithPrio,- makeDefaultStderrRecorder,- toCologActionWithPrio)-import qualified FuzzySearch-import GHC.Stack (emptyCallStack)-import qualified HieDbRetry-import Ide.PluginUtils (pluginDescToIdePlugins)-import Ide.Types-import qualified Language.LSP.Types as LSP-import Language.LSP.Types.Lens (didChangeWatchedFiles,- workspace)-import qualified Language.LSP.Types.Lens as L-import qualified Progress-import System.Time.Extra-import qualified Test.QuickCheck.Monadic as MonadicQuickCheck-import Test.QuickCheck.Monadic (forAllM, monadicIO)-import Test.Tasty-import Test.Tasty.ExpectedFailure-import Test.Tasty.HUnit-import Test.Tasty.Ingredients.Rerun-import Test.Tasty.QuickCheck-import Text.Printf (printf)-import Text.Regex.TDFA ((=~))--data Log- = LogGhcIde Ghcide.Log- | LogIDEMain IDE.Log- | LogVfs VfsLog--instance Pretty Log where- pretty = \case- LogGhcIde log -> pretty log- LogIDEMain log -> pretty log- LogVfs log -> pretty log---- | Wait for the next progress begin step-waitForProgressBegin :: Session ()-waitForProgressBegin = skipManyTill anyMessage $ satisfyMaybe $ \case- FromServerMess SProgress (NotificationMessage _ _ (ProgressParams _ (Begin _))) -> Just ()- _ -> Nothing---- | Wait for the first progress end step--- Also implemented in hls-test-utils Test.Hls-waitForProgressDone :: Session ()-waitForProgressDone = skipManyTill anyMessage $ satisfyMaybe $ \case- FromServerMess SProgress (NotificationMessage _ _ (ProgressParams _ (End _))) -> Just ()- _ -> Nothing---- | Wait for all progress to be done--- Needs at least one progress done notification to return--- Also implemented in hls-test-utils Test.Hls-waitForAllProgressDone :: Session ()-waitForAllProgressDone = loop- where- loop = do- ~() <- skipManyTill anyMessage $ satisfyMaybe $ \case- FromServerMess SProgress (NotificationMessage _ _ (ProgressParams _ (End _))) -> Just ()- _ -> Nothing- done <- null <$> getIncompleteProgressSessions- unless done loop--main :: IO ()-main = do- docWithPriorityRecorder <- makeDefaultStderrRecorder (Just [PriorityColumn, DataColumn])-- let docWithFilteredPriorityRecorder@Recorder{ logger_ } =- docWithPriorityRecorder- & cfilter (\WithPriority{ priority } -> priority >= Debug)-- -- exists so old-style logging works. intended to be phased out- let logger = Logger $ \p m -> logger_ (WithPriority p emptyCallStack (pretty m))-- let recorder = docWithFilteredPriorityRecorder- & cmapWithPrio pretty-- -- We mess with env vars so run single-threaded.- defaultMainWithRerun $ testGroup "ghcide"- [ testSession "open close" $ do- doc <- createDoc "Testing.hs" "haskell" ""- void (skipManyTill anyMessage $ message SWindowWorkDoneProgressCreate)- waitForProgressBegin- closeDoc doc- waitForProgressDone- , initializeResponseTests- , completionTests- , cppTests- , diagnosticTests- , codeLensesTests- , outlineTests- , highlightTests- , findDefinitionAndHoverTests- , pluginSimpleTests- , pluginParsedResultTests- , preprocessorTests- , thTests- , symlinkTests- , safeTests- , unitTests recorder logger- , haddockTests- , positionMappingTests recorder- , watchedFilesTests- , cradleTests- , dependentFileTest- , nonLspCommandLine- , ifaceTests- , bootTests- , rootUriTests- , asyncTests- , clientSettingsTest- , referenceTests- , garbageCollectionTests- , HieDbRetry.tests- ]--initializeResponseTests :: TestTree-initializeResponseTests = withResource acquire release tests where-- -- these tests document and monitor the evolution of the- -- capabilities announced by the server in the initialize- -- response. Currently the server advertises almost no capabilities- -- at all, in some cases failing to announce capabilities that it- -- actually does provide! Hopefully this will change ...- tests :: IO (ResponseMessage Initialize) -> TestTree- tests getInitializeResponse =- testGroup "initialize response capabilities"- [ chk " text doc sync" _textDocumentSync tds- , chk " hover" _hoverProvider (Just $ InL True)- , chk " completion" _completionProvider (Just $ CompletionOptions Nothing (Just ["."]) Nothing (Just True))- , chk "NO signature help" _signatureHelpProvider Nothing- , chk " goto definition" _definitionProvider (Just $ InL True)- , chk " goto type definition" _typeDefinitionProvider (Just $ InL True)- -- BUG in lsp-test, this test fails, just change the accepted response- -- for now- , chk "NO goto implementation" _implementationProvider (Just $ InL False)- , chk " find references" _referencesProvider (Just $ InL True)- , chk " doc highlight" _documentHighlightProvider (Just $ InL True)- , chk " doc symbol" _documentSymbolProvider (Just $ InL True)- , chk " workspace symbol" _workspaceSymbolProvider (Just $ InL True)- , chk " code action" _codeActionProvider (Just $ InL False)- , chk " code lens" _codeLensProvider (Just $ CodeLensOptions (Just False) (Just False))- , chk "NO doc formatting" _documentFormattingProvider (Just $ InL False)- , chk "NO doc range formatting"- _documentRangeFormattingProvider (Just $ InL False)- , chk "NO doc formatting on typing"- _documentOnTypeFormattingProvider Nothing- , chk "NO renaming" _renameProvider (Just $ InL False)- , chk "NO doc link" _documentLinkProvider Nothing- , chk "NO color" (^. L.colorProvider) (Just $ InL False)- , chk "NO folding range" _foldingRangeProvider (Just $ InL False)- , che " execute command" _executeCommandProvider [typeLensCommandId, blockCommandId]- , chk " workspace" (^. L.workspace) (Just $ WorkspaceServerCapabilities (Just WorkspaceFoldersServerCapabilities{_supported = Just True, _changeNotifications = Just ( InR True )}))- , chk "NO experimental" (^. L.experimental) Nothing- ] where-- tds = Just (InL (TextDocumentSyncOptions- { _openClose = Just True- , _change = Just TdSyncIncremental- , _willSave = Nothing- , _willSaveWaitUntil = Nothing- , _save = Just (InR $ SaveOptions {_includeText = Nothing})}))-- chk :: (Eq a, Show a) => TestName -> (ServerCapabilities -> a) -> a -> TestTree- chk title getActual expected =- testCase title $ getInitializeResponse >>= \ir -> expected @=? (getActual . innerCaps) ir-- che :: TestName -> (ServerCapabilities -> Maybe ExecuteCommandOptions) -> [T.Text] -> TestTree- che title getActual expected = testCase title doTest- where- doTest = do- ir <- getInitializeResponse- let Just ExecuteCommandOptions {_commands = List commands} = getActual $ innerCaps ir- commandNames = (!! 2) . T.splitOn ":" <$> commands- zipWithM_ (\e o -> T.isSuffixOf e o @? show (e,o)) (sort expected) (sort commandNames)-- innerCaps :: ResponseMessage Initialize -> ServerCapabilities- innerCaps (ResponseMessage _ _ (Right (InitializeResult c _))) = c- innerCaps (ResponseMessage _ _ (Left _)) = error "Initialization error"-- acquire :: IO (ResponseMessage Initialize)- acquire = run initializeResponse-- release :: ResponseMessage Initialize -> IO ()- release = const $ pure ()---diagnosticTests :: TestTree-diagnosticTests = testGroup "diagnostics"- [ testSessionWait "fix syntax error" $ do- let content = T.unlines [ "module Testing wher" ]- doc <- createDoc "Testing.hs" "haskell" content- expectDiagnostics [("Testing.hs", [(DsError, (0, 15), "parse error")])]- let change = TextDocumentContentChangeEvent- { _range = Just (Range (Position 0 15) (Position 0 19))- , _rangeLength = Nothing- , _text = "where"- }- changeDoc doc [change]- expectDiagnostics [("Testing.hs", [])]- , testSessionWait "introduce syntax error" $ do- let content = T.unlines [ "module Testing where" ]- doc <- createDoc "Testing.hs" "haskell" content- void $ skipManyTill anyMessage (message SWindowWorkDoneProgressCreate)- waitForProgressBegin- let change = TextDocumentContentChangeEvent- { _range = Just (Range (Position 0 15) (Position 0 18))- , _rangeLength = Nothing- , _text = "wher"- }- changeDoc doc [change]- expectDiagnostics [("Testing.hs", [(DsError, (0, 15), "parse error")])]- , testSessionWait "update syntax error" $ do- let content = T.unlines [ "module Testing(missing) where" ]- doc <- createDoc "Testing.hs" "haskell" content- expectDiagnostics [("Testing.hs", [(DsError, (0, 15), "Not in scope: 'missing'")])]- let change = TextDocumentContentChangeEvent- { _range = Just (Range (Position 0 15) (Position 0 16))- , _rangeLength = Nothing- , _text = "l"- }- changeDoc doc [change]- expectDiagnostics [("Testing.hs", [(DsError, (0, 15), "Not in scope: 'lissing'")])]- , testSessionWait "variable not in scope" $ do- let content = T.unlines- [ "module Testing where"- , "foo :: Int -> Int -> Int"- , "foo a _b = a + ab"- , "bar :: Int -> Int -> Int"- , "bar _a b = cd + b"- ]- _ <- createDoc "Testing.hs" "haskell" content- expectDiagnostics- [ ( "Testing.hs"- , [ (DsError, (2, 15), "Variable not in scope: ab")- , (DsError, (4, 11), "Variable not in scope: cd")- ]- )- ]- , testSessionWait "type error" $ do- let content = T.unlines- [ "module Testing where"- , "foo :: Int -> String -> Int"- , "foo a b = a + b"- ]- _ <- createDoc "Testing.hs" "haskell" content- expectDiagnostics- [ ( "Testing.hs"- , [(DsError, (2, 14), "Couldn't match type '[Char]' with 'Int'")]- )- ]- , testSessionWait "typed hole" $ do- let content = T.unlines- [ "module Testing where"- , "foo :: Int -> String"- , "foo a = _ a"- ]- _ <- createDoc "Testing.hs" "haskell" content- expectDiagnostics- [ ( "Testing.hs"- , [(DsError, (2, 8), "Found hole: _ :: Int -> String")]- )- ]-- , testGroup "deferral" $- let sourceA a = T.unlines- [ "module A where"- , "a :: Int"- , "a = " <> a]- sourceB = T.unlines- [ "module B where"- , "import A ()"- , "b :: Float"- , "b = True"]- bMessage = "Couldn't match expected type 'Float' with actual type 'Bool'"- expectedDs aMessage =- [ ("A.hs", [(DsError, (2,4), aMessage)])- , ("B.hs", [(DsError, (3,4), bMessage)])]- deferralTest title binding msg = testSessionWait title $ do- _ <- createDoc "A.hs" "haskell" $ sourceA binding- _ <- createDoc "B.hs" "haskell" sourceB- expectDiagnostics $ expectedDs msg- in- [ deferralTest "type error" "True" "Couldn't match expected type"- , deferralTest "typed hole" "_" "Found hole"- , deferralTest "out of scope var" "unbound" "Variable not in scope"- ]-- , testSessionWait "remove required module" $ do- let contentA = T.unlines [ "module ModuleA where" ]- docA <- createDoc "ModuleA.hs" "haskell" contentA- let contentB = T.unlines- [ "module ModuleB where"- , "import ModuleA"- ]- _ <- createDoc "ModuleB.hs" "haskell" contentB- let change = TextDocumentContentChangeEvent- { _range = Just (Range (Position 0 0) (Position 0 20))- , _rangeLength = Nothing- , _text = ""- }- changeDoc docA [change]- expectDiagnostics [("ModuleB.hs", [(DsError, (1, 0), "Could not find module")])]- , testSessionWait "add missing module" $ do- let contentB = T.unlines- [ "module ModuleB where"- , "import ModuleA ()"- ]- _ <- createDoc "ModuleB.hs" "haskell" contentB- expectDiagnostics [("ModuleB.hs", [(DsError, (1, 7), "Could not find module")])]- let contentA = T.unlines [ "module ModuleA where" ]- _ <- createDoc "ModuleA.hs" "haskell" contentA- expectDiagnostics [("ModuleB.hs", [])]- , testCase "add missing module (non workspace)" $- -- By default lsp-test sends FileWatched notifications for all files, which we don't want- -- as non workspace modules will not be watched by the LSP server.- -- To work around this, we tell lsp-test that our client doesn't have the- -- FileWatched capability, which is enough to disable the notifications- withTempDir $ \tmpDir -> runInDir'' lspTestCapsNoFileWatches tmpDir "." "." [] $ do- let contentB = T.unlines- [ "module ModuleB where"- , "import ModuleA ()"- ]- _ <- createDoc (tmpDir </> "ModuleB.hs") "haskell" contentB- expectDiagnostics [(tmpDir </> "ModuleB.hs", [(DsError, (1, 7), "Could not find module")])]- let contentA = T.unlines [ "module ModuleA where" ]- _ <- createDoc (tmpDir </> "ModuleA.hs") "haskell" contentA- expectDiagnostics [(tmpDir </> "ModuleB.hs", [])]- , testSessionWait "cyclic module dependency" $ do- let contentA = T.unlines- [ "module ModuleA where"- , "import ModuleB"- ]- let contentB = T.unlines- [ "module ModuleB where"- , "import ModuleA"- ]- _ <- createDoc "ModuleA.hs" "haskell" contentA- _ <- createDoc "ModuleB.hs" "haskell" contentB- expectDiagnostics- [ ( "ModuleA.hs"- , [(DsError, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]- )- , ( "ModuleB.hs"- , [(DsError, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]- )- ]- , testSession' "deeply nested cyclic module dependency" $ \path -> do- let contentA = unlines- [ "module ModuleA where" , "import ModuleB" ]- let contentB = unlines- [ "module ModuleB where" , "import ModuleA" ]- let contentC = unlines- [ "module ModuleC where" , "import ModuleB" ]- let contentD = T.unlines- [ "module ModuleD where" , "import ModuleC" ]- cradle =- "cradle: {direct: {arguments: [ModuleA, ModuleB, ModuleC, ModuleD]}}"- liftIO $ writeFile (path </> "ModuleA.hs") contentA- liftIO $ writeFile (path </> "ModuleB.hs") contentB- liftIO $ writeFile (path </> "ModuleC.hs") contentC- liftIO $ writeFile (path </> "hie.yaml") cradle- _ <- createDoc "ModuleD.hs" "haskell" contentD- expectDiagnostics- [ ( "ModuleB.hs"- , [(DsError, (1, 7), "Cyclic module dependency between ModuleA, ModuleB")]- )- ]- , testSessionWait "cyclic module dependency with hs-boot" $ do- let contentA = T.unlines- [ "module ModuleA where"- , "import {-# SOURCE #-} ModuleB"- ]- let contentB = T.unlines- [ "{-# OPTIONS -Wmissing-signatures#-}"- , "module ModuleB where"- , "import ModuleA"- -- introduce an artificial diagnostic- , "foo = ()"- ]- let contentBboot = T.unlines- [ "module ModuleB where"- ]- _ <- createDoc "ModuleA.hs" "haskell" contentA- _ <- createDoc "ModuleB.hs" "haskell" contentB- _ <- createDoc "ModuleB.hs-boot" "haskell" contentBboot- expectDiagnostics [("ModuleB.hs", [(DsWarning, (3,0), "Top-level binding")])]- , testSessionWait "correct reference used with hs-boot" $ do- let contentB = T.unlines- [ "module ModuleB where"- , "import {-# SOURCE #-} ModuleA()"- ]- let contentA = T.unlines- [ "module ModuleA where"- , "import ModuleB()"- , "x = 5"- ]- let contentAboot = T.unlines- [ "module ModuleA where"- ]- let contentC = T.unlines- [ "{-# OPTIONS -Wmissing-signatures #-}"- , "module ModuleC where"- , "import ModuleA"- -- this reference will fail if it gets incorrectly- -- resolved to the hs-boot file- , "y = x"- ]- _ <- createDoc "ModuleB.hs" "haskell" contentB- _ <- createDoc "ModuleA.hs" "haskell" contentA- _ <- createDoc "ModuleA.hs-boot" "haskell" contentAboot- _ <- createDoc "ModuleC.hs" "haskell" contentC- expectDiagnostics [("ModuleC.hs", [(DsWarning, (3,0), "Top-level binding")])]- , testSessionWait "redundant import" $ do- let contentA = T.unlines ["module ModuleA where"]- let contentB = T.unlines- [ "{-# OPTIONS_GHC -Wunused-imports #-}"- , "module ModuleB where"- , "import ModuleA"- ]- _ <- createDoc "ModuleA.hs" "haskell" contentA- _ <- createDoc "ModuleB.hs" "haskell" contentB- expectDiagnosticsWithTags- [ ( "ModuleB.hs"- , [(DsWarning, (2, 0), "The import of 'ModuleA' is redundant", Just DtUnnecessary)]- )- ]- , testSessionWait "redundant import even without warning" $ do- let contentA = T.unlines ["module ModuleA where"]- let contentB = T.unlines- [ "{-# OPTIONS_GHC -Wno-unused-imports -Wmissing-signatures #-}"- , "module ModuleB where"- , "import ModuleA"- -- introduce an artificial warning for testing purposes- , "foo = ()"- ]- _ <- createDoc "ModuleA.hs" "haskell" contentA- _ <- createDoc "ModuleB.hs" "haskell" contentB- expectDiagnostics [("ModuleB.hs", [(DsWarning, (3,0), "Top-level binding")])]- , testSessionWait "package imports" $ do- let thisDataListContent = T.unlines- [ "module Data.List where"- , "x :: Integer"- , "x = 123"- ]- let mainContent = T.unlines- [ "{-# LANGUAGE PackageImports #-}"- , "module Main where"- , "import qualified \"this\" Data.List as ThisList"- , "import qualified \"base\" Data.List as BaseList"- , "useThis = ThisList.x"- , "useBase = BaseList.map"- , "wrong1 = ThisList.map"- , "wrong2 = BaseList.x"- , "main = pure ()"- ]- _ <- createDoc "Data/List.hs" "haskell" thisDataListContent- _ <- createDoc "Main.hs" "haskell" mainContent- expectDiagnostics- [ ( "Main.hs"- , [(DsError, (6, 9),- if ghcVersion >= GHC96 then- "Variable not in scope: ThisList.map"- else if ghcVersion >= GHC94 then- "Variable not in scope: map" -- See https://gitlab.haskell.org/ghc/ghc/-/issues/22130- else- "Not in scope: \8216ThisList.map\8217")- ,(DsError, (7, 9),- if ghcVersion >= GHC96 then- "Variable not in scope: BaseList.x"- else if ghcVersion >= GHC94 then- "Variable not in scope: x" -- See https://gitlab.haskell.org/ghc/ghc/-/issues/22130- else- "Not in scope: \8216BaseList.x\8217")- ]- )- ]- , testSessionWait "unqualified warnings" $ do- let fooContent = T.unlines- [ "{-# OPTIONS_GHC -Wredundant-constraints #-}"- , "module Foo where"- , "foo :: Ord a => a -> Int"- , "foo _a = 1"- ]- _ <- createDoc "Foo.hs" "haskell" fooContent- expectDiagnostics- [ ( "Foo.hs"- -- The test is to make sure that warnings contain unqualified names- -- where appropriate. The warning should use an unqualified name 'Ord', not- -- something like 'GHC.Classes.Ord'. The choice of redundant-constraints to- -- test this is fairly arbitrary.- , [(DsWarning, (2, if ghcVersion >= GHC94 then 7 else 0), "Redundant constraint: Ord a")- ]- )- ]- , testSessionWait "lower-case drive" $ do- let aContent = T.unlines- [ "module A.A where"- , "import A.B ()"- ]- bContent = T.unlines- [ "{-# OPTIONS_GHC -Wall #-}"- , "module A.B where"- , "import Data.List"- ]- uriB <- getDocUri "A/B.hs"- Just pathB <- pure $ uriToFilePath uriB- uriB <- pure $- let (drive, suffix) = splitDrive pathB- in filePathToUri (joinDrive (lower drive) suffix)- liftIO $ createDirectoryIfMissing True (takeDirectory pathB)- liftIO $ writeFileUTF8 pathB $ T.unpack bContent- uriA <- getDocUri "A/A.hs"- Just pathA <- pure $ uriToFilePath uriA- uriA <- pure $- let (drive, suffix) = splitDrive pathA- in filePathToUri (joinDrive (lower drive) suffix)- let itemA = TextDocumentItem uriA "haskell" 0 aContent- let a = TextDocumentIdentifier uriA- sendNotification STextDocumentDidOpen (DidOpenTextDocumentParams itemA)- NotificationMessage{_params = PublishDiagnosticsParams fileUri _ diags} <- skipManyTill anyMessage diagnostic- -- Check that if we put a lower-case drive in for A.A- -- the diagnostics for A.B will also be lower-case.- liftIO $ fileUri @?= uriB- let msg = head (toList diags) ^. L.message- liftIO $ unless ("redundant" `T.isInfixOf` msg) $- assertFailure ("Expected redundant import but got " <> T.unpack msg)- closeDoc a- , testSessionWait "haddock parse error" $ do- let fooContent = T.unlines- [ "module Foo where"- , "foo :: Int"- , "foo = 1 {-|-}"- ]- _ <- createDoc "Foo.hs" "haskell" fooContent- if ghcVersion >= GHC90 then- -- Haddock parse errors are ignored on ghc-9.0- pure ()- else- expectDiagnostics- [ ( "Foo.hs"- , [(DsWarning, (2, 8), "Haddock parse error on input")]- )- ]- , testSessionWait "strip file path" $ do- let- name = "Testing"- content = T.unlines- [ "module " <> name <> " where"- , "value :: Maybe ()"- , "value = [()]"- ]- _ <- createDoc (T.unpack name <> ".hs") "haskell" content- notification <- skipManyTill anyMessage diagnostic- let- offenders =- Lsp.params .- Lsp.diagnostics .- Lens.folded .- Lsp.message .- Lens.filtered (T.isInfixOf ("/" <> name <> ".hs:"))- failure msg = liftIO $ assertFailure $ "Expected file path to be stripped but got " <> T.unpack msg- Lens.mapMOf_ offenders failure notification- , testSession' "-Werror in cradle is ignored" $ \sessionDir -> do- liftIO $ writeFile (sessionDir </> "hie.yaml")- "cradle: {direct: {arguments: [\"-Wall\", \"-Werror\"]}}"- let fooContent = T.unlines- [ "module Foo where"- , "foo = ()"- ]- _ <- createDoc "Foo.hs" "haskell" fooContent- expectDiagnostics- [ ( "Foo.hs"- , [(DsWarning, (1, 0), "Top-level binding with no type signature:")- ]- )- ]- , testSessionWait "-Werror in pragma is ignored" $ do- let fooContent = T.unlines- [ "{-# OPTIONS_GHC -Wall -Werror #-}"- , "module Foo() where"- , "foo :: Int"- , "foo = 1"- ]- _ <- createDoc "Foo.hs" "haskell" fooContent- expectDiagnostics- [ ( "Foo.hs"- , [(DsWarning, (3, 0), "Defined but not used:")- ]- )- ]- , testCase "typecheck-all-parents-of-interest" $ runWithExtraFiles "recomp" $ \dir -> do- let bPath = dir </> "B.hs"- pPath = dir </> "P.hs"- aPath = dir </> "A.hs"-- bSource <- liftIO $ readFileUtf8 bPath -- y :: Int- pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int- aSource <- liftIO $ readFileUtf8 aPath -- x = y :: Int-- bdoc <- createDoc bPath "haskell" bSource- _pdoc <- createDoc pPath "haskell" pSource- expectDiagnostics- [("P.hs", [(DsWarning,(4,0), "Top-level binding")])] -- So that we know P has been loaded-- -- Change y from Int to B which introduces a type error in A (imported from P)- changeDoc bdoc [TextDocumentContentChangeEvent Nothing Nothing $- T.unlines ["module B where", "y :: Bool", "y = undefined"]]- expectDiagnostics- [("A.hs", [(DsError, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])- ]-- -- Open A and edit to fix the type error- adoc <- createDoc aPath "haskell" aSource- changeDoc adoc [TextDocumentContentChangeEvent Nothing Nothing $- T.unlines ["module A where", "import B", "x :: Bool", "x = y"]]-- expectDiagnostics- [ ( "P.hs",- [ (DsError, (4, 6), "Couldn't match expected type 'Int' with actual type 'Bool'"),- (DsWarning, (4, 0), "Top-level binding")- ]- ),- ("A.hs", [])- ]- expectNoMoreDiagnostics 1-- , testSessionWait "deduplicate missing module diagnostics" $ do- let fooContent = T.unlines [ "module Foo() where" , "import MissingModule" ]- doc <- createDoc "Foo.hs" "haskell" fooContent- expectDiagnostics [("Foo.hs", [(DsError, (1,7), "Could not find module 'MissingModule'")])]-- changeDoc doc [TextDocumentContentChangeEvent Nothing Nothing "module Foo() where" ]- expectDiagnostics []-- changeDoc doc [TextDocumentContentChangeEvent Nothing Nothing $ T.unlines- [ "module Foo() where" , "import MissingModule" ] ]- expectDiagnostics [("Foo.hs", [(DsError, (1,7), "Could not find module 'MissingModule'")])]-- , testGroup "Cancellation"- [ cancellationTestGroup "edit header" editHeader yesSession noParse noTc- , cancellationTestGroup "edit import" editImport noSession yesParse noTc- , cancellationTestGroup "edit body" editBody yesSession yesParse yesTc- ]- ]- where- editPair x y = let p = Position x y ; p' = Position x (y+2) in- (TextDocumentContentChangeEvent {_range=Just (Range p p), _rangeLength=Nothing, _text="fd"}- ,TextDocumentContentChangeEvent {_range=Just (Range p p'), _rangeLength=Nothing, _text=""})- editHeader = editPair 0 0- editImport = editPair 2 10- editBody = editPair 3 10-- noParse = False- yesParse = True-- noSession = False- yesSession = True-- noTc = False- yesTc = True--cancellationTestGroup :: TestName -> (TextDocumentContentChangeEvent, TextDocumentContentChangeEvent) -> Bool -> Bool -> Bool -> TestTree-cancellationTestGroup name edits sessionDepsOutcome parseOutcome tcOutcome = testGroup name- [ cancellationTemplate edits Nothing- , cancellationTemplate edits $ Just ("GetFileContents", True)- , cancellationTemplate edits $ Just ("GhcSession", True)- -- the outcome for GetModSummary is always True because parseModuleHeader never fails (!)- , cancellationTemplate edits $ Just ("GetModSummary", True)- , cancellationTemplate edits $ Just ("GetModSummaryWithoutTimestamps", True)- -- getLocatedImports never fails- , cancellationTemplate edits $ Just ("GetLocatedImports", True)- , cancellationTemplate edits $ Just ("GhcSessionDeps", sessionDepsOutcome)- , cancellationTemplate edits $ Just ("GetParsedModule", parseOutcome)- , cancellationTemplate edits $ Just ("TypeCheck", tcOutcome)- , cancellationTemplate edits $ Just ("GetHieAst", tcOutcome)- ]--cancellationTemplate :: (TextDocumentContentChangeEvent, TextDocumentContentChangeEvent) -> Maybe (String, Bool) -> TestTree-cancellationTemplate (edit, undoEdit) mbKey = testCase (maybe "-" fst mbKey) $ runTestNoKick $ do- doc <- createDoc "Foo.hs" "haskell" $ T.unlines- [ "{-# OPTIONS_GHC -Wall #-}"- , "module Foo where"- , "import Data.List()"- , "f0 x = (x,x)"- ]-- -- for the example above we expect one warning- let missingSigDiags = [(DsWarning, (3, 0), "Top-level binding") ]- typeCheck doc >> expectCurrentDiagnostics doc missingSigDiags-- -- Now we edit the document and wait for the given key (if any)- changeDoc doc [edit]- whenJust mbKey $ \(key, expectedResult) -> do- WaitForIdeRuleResult{ideResultSuccess} <- waitForAction key doc- liftIO $ ideResultSuccess @?= expectedResult-- -- The 2nd edit cancels the active session and unbreaks the file- -- wait for typecheck and check that the current diagnostics are accurate- changeDoc doc [undoEdit]- typeCheck doc >> expectCurrentDiagnostics doc missingSigDiags-- expectNoMoreDiagnostics 0.5- where- -- similar to run except it disables kick- runTestNoKick s = withTempDir $ \dir -> runInDir' dir "." "." ["--test-no-kick"] s-- typeCheck doc = do- WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc- liftIO $ assertBool "The file should typecheck" ideResultSuccess- -- wait for the debouncer to publish diagnostics if the rule runs- liftIO $ sleep 0.2- -- flush messages to ensure current diagnostics state is updated- flushMessages--codeLensesTests :: TestTree-codeLensesTests = testGroup "code lenses"- [ addSigLensesTests- ]--watchedFilesTests :: TestTree-watchedFilesTests = testGroup "watched files"- [ testGroup "Subscriptions"- [ testSession' "workspace files" $ \sessionDir -> do- liftIO $ writeFile (sessionDir </> "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"WatchedFilesMissingModule\"]}}"- _doc <- createDoc "A.hs" "haskell" "{-#LANGUAGE NoImplicitPrelude #-}\nmodule A where\nimport WatchedFilesMissingModule"- watchedFileRegs <- getWatchedFilesSubscriptionsUntil STextDocumentPublishDiagnostics-- -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle- liftIO $ length watchedFileRegs @?= 2-- , testSession' "non workspace file" $ \sessionDir -> do- tmpDir <- liftIO getTemporaryDirectory- let yaml = "cradle: {direct: {arguments: [\"-i" <> tail(init(show tmpDir)) <> "\", \"A\", \"WatchedFilesMissingModule\"]}}"- liftIO $ writeFile (sessionDir </> "hie.yaml") yaml- _doc <- createDoc "A.hs" "haskell" "{-# LANGUAGE NoImplicitPrelude#-}\nmodule A where\nimport WatchedFilesMissingModule"- watchedFileRegs <- getWatchedFilesSubscriptionsUntil STextDocumentPublishDiagnostics-- -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle- liftIO $ length watchedFileRegs @?= 2-- -- TODO add a test for didChangeWorkspaceFolder- ]- , testGroup "Changes"- [- testSession' "workspace files" $ \sessionDir -> do- liftIO $ writeFile (sessionDir </> "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"B\"]}}"- liftIO $ writeFile (sessionDir </> "B.hs") $ unlines- ["module B where"- ,"b :: Bool"- ,"b = False"]- _doc <- createDoc "A.hs" "haskell" $ T.unlines- ["module A where"- ,"import B"- ,"a :: ()"- ,"a = b"- ]- expectDiagnostics [("A.hs", [(DsError, (3, 4), "Couldn't match expected type '()' with actual type 'Bool'")])]- -- modify B off editor- liftIO $ writeFile (sessionDir </> "B.hs") $ unlines- ["module B where"- ,"b :: Int"- ,"b = 0"]- sendNotification SWorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $- List [FileEvent (filePathToUri $ sessionDir </> "B.hs") FcChanged ]- expectDiagnostics [("A.hs", [(DsError, (3, 4), "Couldn't match expected type '()' with actual type 'Int'")])]- ]- ]--addSigLensesTests :: TestTree-addSigLensesTests =- let pragmas = "{-# OPTIONS_GHC -Wmissing-signatures -Wmissing-pattern-synonym-signatures #-}"- moduleH exported =- T.unlines- [ "{-# LANGUAGE PatternSynonyms,TypeApplications,DataKinds,RankNTypes,ScopedTypeVariables,TypeOperators,GADTs,BangPatterns #-}"- , "module Sigs(" <> exported <> ") where"- , "import qualified Data.Complex as C"- , "import Data.Data (Proxy (..), type (:~:) (..), mkCharType)"- , "data T1 a where"- , " MkT1 :: (Show b) => a -> b -> T1 a"- ]- before enableGHCWarnings exported (def, _) others =- T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported, def] <> others- after' enableGHCWarnings exported (def, sig) others =- T.unlines $ [pragmas | enableGHCWarnings] <> [moduleH exported] <> maybe [] pure sig <> [def] <> others- createConfig mode = A.object ["haskell" A..= A.object ["plugin" A..= A.object ["ghcide-type-lenses" A..= A.object ["config" A..= A.object ["mode" A..= A.String mode]]]]]- sigSession testName enableGHCWarnings mode exported def others = testSession testName $ do- let originalCode = before enableGHCWarnings exported def others- let expectedCode = after' enableGHCWarnings exported def others- sendNotification SWorkspaceDidChangeConfiguration $ DidChangeConfigurationParams $ createConfig mode- doc <- createDoc "Sigs.hs" "haskell" originalCode- waitForProgressDone- codeLenses <- getCodeLenses doc- if not $ null $ snd def- then do- liftIO $ length codeLenses == 1 @? "Expected 1 code lens, but got: " <> show codeLenses- executeCommand $ fromJust $ head codeLenses ^. L.command- modifiedCode <- skipManyTill anyMessage (getDocumentEdit doc)- liftIO $ expectedCode @=? modifiedCode- else liftIO $ null codeLenses @? "Expected no code lens, but got: " <> show codeLenses- cases =- [ ("abc = True", "abc :: Bool")- , ("foo a b = a + b", "foo :: Num a => a -> a -> a")- , ("bar a b = show $ a + b", "bar :: (Show a, Num a) => a -> a -> String")- , ("(!!!) a b = a > b", "(!!!) :: Ord a => a -> a -> Bool")- , ("a >>>> b = a + b", "(>>>>) :: Num a => a -> a -> a")- , ("a `haha` b = a b", "haha :: (t1 -> t2) -> t1 -> t2")- , ("pattern Some a = Just a", "pattern Some :: a -> Maybe a")- , ("pattern Some a <- Just a", "pattern Some :: a -> Maybe a")- , ("pattern Some a <- Just a\n where Some a = Just a", "pattern Some :: a -> Maybe a")- , ("pattern Some a <- Just !a\n where Some !a = Just a", "pattern Some :: a -> Maybe a")- , ("pattern Point{x, y} = (x, y)", "pattern Point :: a -> b -> (a, b)")- , ("pattern Point{x, y} <- (x, y)", "pattern Point :: a -> b -> (a, b)")- , ("pattern Point{x, y} <- (x, y)\n where Point x y = (x, y)", "pattern Point :: a -> b -> (a, b)")- , ("pattern MkT1' b = MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a")- , ("pattern MkT1' b <- MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a")- , ("pattern MkT1' b <- MkT1 42 b\n where MkT1' b = MkT1 42 b", "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a")- , ("qualifiedSigTest= C.realPart", "qualifiedSigTest :: C.Complex a -> a")- , ("head = 233", "head :: Integer")- , ("rank2Test (k :: forall a . a -> a) = (k 233 :: Int, k \"QAQ\")", "rank2Test :: (forall a. a -> a) -> (Int, " <> listOfChar <> ")")- , ("symbolKindTest = Proxy @\"qwq\"", "symbolKindTest :: Proxy \"qwq\"")- , ("promotedKindTest = Proxy @Nothing", if ghcVersion >= GHC96 then "promotedKindTest :: Proxy Nothing" else "promotedKindTest :: Proxy 'Nothing")- , ("typeOperatorTest = Refl", if ghcVersion >= GHC92 then "typeOperatorTest :: forall {k} {a :: k}. a :~: a" else "typeOperatorTest :: a :~: a")- , ("notInScopeTest = mkCharType", "notInScopeTest :: String -> Data.Data.DataType")- ]- in testGroup- "add signature"- [ testGroup "signatures are correct" [sigSession (T.unpack $ T.replace "\n" "\\n" def) False "always" "" (def, Just sig) [] | (def, sig) <- cases]- , sigSession "exported mode works" False "exported" "xyz" ("xyz = True", Just "xyz :: Bool") (fst <$> take 3 cases)- , testGroup- "diagnostics mode works"- [ sigSession "with GHC warnings" True "diagnostics" "" (second Just $ head cases) []- , sigSession "without GHC warnings" False "diagnostics" "" (second (const Nothing) $ head cases) []- ]- , testSession "keep stale lens" $ do- let content = T.unlines- [ "module Stale where"- , "f = _"- ]- doc <- createDoc "Stale.hs" "haskell" content- oldLens <- getCodeLenses doc- liftIO $ length oldLens @?= 1- let edit = TextEdit (mkRange 0 4 0 5) "" -- Remove the `_`- _ <- applyEdit doc edit- newLens <- getCodeLenses doc- liftIO $ newLens @?= oldLens- ]--linkToLocation :: [LocationLink] -> [Location]-linkToLocation = map (\LocationLink{_targetUri,_targetRange} -> Location _targetUri _targetRange)--checkDefs :: [Location] |? [LocationLink] -> Session [Expect] -> Session ()-checkDefs (either id linkToLocation . toEither -> defs) mkExpectations = traverse_ check =<< mkExpectations where- check (ExpectRange expectedRange) = do- assertNDefinitionsFound 1 defs- assertRangeCorrect (head defs) expectedRange- check (ExpectLocation expectedLocation) = do- assertNDefinitionsFound 1 defs- liftIO $ do- canonActualLoc <- canonicalizeLocation (head defs)- canonExpectedLoc <- canonicalizeLocation expectedLocation- canonActualLoc @?= canonExpectedLoc- check ExpectNoDefinitions = do- assertNDefinitionsFound 0 defs- check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file"- check _ = pure () -- all other expectations not relevant to getDefinition-- assertNDefinitionsFound :: Int -> [a] -> Session ()- assertNDefinitionsFound n defs = liftIO $ assertEqual "number of definitions" n (length defs)-- assertRangeCorrect Location{_range = foundRange} expectedRange =- liftIO $ expectedRange @=? foundRange--canonicalizeLocation :: Location -> IO Location-canonicalizeLocation (Location uri range) = Location <$> canonicalizeUri uri <*> pure range--findDefinitionAndHoverTests :: TestTree-findDefinitionAndHoverTests = let-- tst :: (TextDocumentIdentifier -> Position -> Session a, a -> Session [Expect] -> Session ()) -> Position -> String -> Session [Expect] -> String -> TestTree- tst (get, check) pos sfp targetRange title = testSessionWithExtraFiles "hover" title $ \dir -> do-- -- Dirty the cache to check that definitions work even in the presence of iface files- liftIO $ runInDir dir $ do- let fooPath = dir </> "Foo.hs"- fooSource <- liftIO $ readFileUtf8 fooPath- fooDoc <- createDoc fooPath "haskell" fooSource- _ <- getHover fooDoc $ Position 4 3- closeDoc fooDoc-- doc <- openTestDataDoc (dir </> sfp)- waitForProgressDone- found <- get doc pos- check found targetRange---- checkHover :: Maybe Hover -> Session [Expect] -> Session ()- checkHover hover expectations = traverse_ check =<< expectations where-- check expected =- case hover of- Nothing -> unless (expected == ExpectNoHover) $ liftIO $ assertFailure "no hover found"- Just Hover{_contents = (HoverContents MarkupContent{_value = standardizeQuotes -> msg})- ,_range = rangeInHover } ->- case expected of- ExpectRange expectedRange -> checkHoverRange expectedRange rangeInHover msg- ExpectHoverRange expectedRange -> checkHoverRange expectedRange rangeInHover msg- ExpectHoverText snippets -> liftIO $ traverse_ (`assertFoundIn` msg) snippets- ExpectHoverExcludeText snippets -> liftIO $ traverse_ (`assertNotFoundIn` msg) snippets- ExpectHoverTextRegex re -> liftIO $ assertBool ("Regex not found in " <> T.unpack msg) (msg =~ re :: Bool)- ExpectNoHover -> liftIO $ assertFailure $ "Expected no hover but got " <> show hover- _ -> pure () -- all other expectations not relevant to hover- _ -> liftIO $ assertFailure $ "test not expecting this kind of hover info" <> show hover-- extractLineColFromHoverMsg :: T.Text -> [T.Text]- extractLineColFromHoverMsg = T.splitOn ":" . head . T.splitOn "*" . last . T.splitOn (sourceFileName <> ":")-- checkHoverRange :: Range -> Maybe Range -> T.Text -> Session ()- checkHoverRange expectedRange rangeInHover msg =- let- lineCol = extractLineColFromHoverMsg msg- -- looks like hovers use 1-based numbering while definitions use 0-based- -- turns out that they are stored 1-based in RealSrcLoc by GHC itself.- adjust Position{_line = l, _character = c} =- Position{_line = l + 1, _character = c + 1}- in- case map (read . T.unpack) lineCol of- [l,c] -> liftIO $ adjust (_start expectedRange) @=? Position l c- _ -> liftIO $ assertFailure $- "expected: " <> show ("[...]" <> sourceFileName <> ":<LINE>:<COL>**[...]", Just expectedRange) <>- "\n but got: " <> show (msg, rangeInHover)-- assertFoundIn :: T.Text -> T.Text -> Assertion- assertFoundIn part whole = assertBool- (T.unpack $ "failed to find: `" <> part <> "` in hover message:\n" <> whole)- (part `T.isInfixOf` whole)-- assertNotFoundIn :: T.Text -> T.Text -> Assertion- assertNotFoundIn part whole = assertBool- (T.unpack $ "found unexpected: `" <> part <> "` in hover message:\n" <> whole)- (not . T.isInfixOf part $ whole)-- sourceFilePath = T.unpack sourceFileName- sourceFileName = "GotoHover.hs"-- mkFindTests tests = testGroup "get"- [ testGroup "definition" $ mapMaybe fst tests- , testGroup "hover" $ mapMaybe snd tests- , checkFileCompiles sourceFilePath $- expectDiagnostics- [ ( "GotoHover.hs", [(DsError, (62, 7), "Found hole: _")])- , ( "GotoHover.hs", [(DsError, (65, 8), "Found hole: _")])- ]- , testGroup "type-definition" typeDefinitionTests- , testGroup "hover-record-dot-syntax" recordDotSyntaxTests ]-- typeDefinitionTests = [ tst (getTypeDefinitions, checkDefs) aaaL14 sourceFilePath (pure tcData) "Saturated data con"- , tst (getTypeDefinitions, checkDefs) aL20 sourceFilePath (pure [ExpectNoDefinitions]) "Polymorphic variable"]-- recordDotSyntaxTests- | ghcVersion >= GHC92 =- [ tst (getHover, checkHover) (Position 19 24) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["x :: MyRecord"]]) "hover over parent"- , tst (getHover, checkHover) (Position 19 25) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over dot shows child"- , tst (getHover, checkHover) (Position 19 26) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over child"- ]- | otherwise = []-- test runDef runHover look expect = testM runDef runHover look (return expect)-- testM runDef runHover look expect title =- ( runDef $ tst def look sourceFilePath expect title- , runHover $ tst hover look sourceFilePath expect title ) where- def = (getDefinitions, checkDefs)- hover = (getHover , checkHover)-- -- search locations expectations on results- fffL4 = _start fffR ; fffR = mkRange 8 4 8 7 ; fff = [ExpectRange fffR]- fffL8 = Position 12 4 ;- fffL14 = Position 18 7 ;- aL20 = Position 19 15- aaaL14 = Position 18 20 ; aaa = [mkR 11 0 11 3]- dcL7 = Position 11 11 ; tcDC = [mkR 7 23 9 16]- dcL12 = Position 16 11 ;- xtcL5 = Position 9 11 ; xtc = [ExpectExternFail, ExpectHoverText ["Int", "Defined in ", "GHC.Types", "ghc-prim"]]- tcL6 = Position 10 11 ; tcData = [mkR 7 0 9 16, ExpectHoverText ["TypeConstructor", "GotoHover.hs:8:1"]]- vvL16 = Position 20 12 ; vv = [mkR 20 4 20 6]- opL16 = Position 20 15 ; op = [mkR 21 2 21 4]- opL18 = Position 22 22 ; opp = [mkR 22 13 22 17]- aL18 = Position 22 20 ; apmp = [mkR 22 10 22 11]- b'L19 = Position 23 13 ; bp = [mkR 23 6 23 7]- xvL20 = Position 24 8 ; xvMsg = [ExpectExternFail, ExpectHoverText ["pack", ":: String -> Text", "Data.Text", "text"]]- clL23 = Position 27 11 ; cls = [mkR 25 0 26 20, ExpectHoverText ["MyClass", "GotoHover.hs:26:1"]]- clL25 = Position 29 9- eclL15 = Position 19 8 ; ecls = [ExpectExternFail, ExpectHoverText ["Num", "Defined in ", "GHC.Num", "base"]]- dnbL29 = Position 33 18 ; dnb = [ExpectHoverText [":: ()"], mkR 33 12 33 21]- dnbL30 = Position 34 23- lcbL33 = Position 37 26 ; lcb = [ExpectHoverText [":: Char"], mkR 37 26 37 27]- lclL33 = Position 37 22- mclL36 = Position 40 1 ; mcl = [mkR 40 0 40 14]- mclL37 = Position 41 1- spaceL37 = Position 41 24 ; space = [ExpectNoDefinitions, ExpectHoverText [":: Char"]]- docL41 = Position 45 1 ; doc = [ExpectHoverText ["Recognizable docs: kpqz"]]- ; constr = [ExpectHoverText ["Monad m"]]- eitL40 = Position 44 28 ; kindE = [ExpectHoverText [if ghcVersion >= GHC92 then ":: Type -> Type -> Type\n" else ":: * -> * -> *\n"]]- intL40 = Position 44 34 ; kindI = [ExpectHoverText [if ghcVersion >= GHC92 then ":: Type\n" else ":: *\n"]]- tvrL40 = Position 44 37 ; kindV = [ExpectHoverText [":: * -> *\n"]]- intL41 = Position 45 20 ; litI = [ExpectHoverText ["7518"]]- chrL36 = Position 41 24 ; litC = [ExpectHoverText ["'f'"]]- txtL8 = Position 12 14 ; litT = [ExpectHoverText ["\"dfgy\""]]- lstL43 = Position 47 12 ; litL = [ExpectHoverText ["[8391 :: Int, 6268]"]]- outL45 = Position 49 3 ; outSig = [ExpectHoverText ["outer", "Bool"], mkR 50 0 50 5]- innL48 = Position 52 5 ; innSig = [ExpectHoverText ["inner", "Char"], mkR 49 2 49 7]- holeL60 = Position 62 7 ; hleInfo = [ExpectHoverText ["_ ::"]]- holeL65 = Position 65 8 ; hleInfo2 = [ExpectHoverText ["_ :: a -> Maybe a"]]- cccL17 = Position 17 16 ; docLink = [ExpectHoverTextRegex "\\*Defined in 'GHC.Types'\\* \\*\\(ghc-prim-[0-9.]+\\)\\*\n\n"]- imported = Position 56 13 ; importedSig = getDocUri "Foo.hs" >>= \foo -> return [ExpectHoverText ["foo", "Foo", "Haddock"], mkL foo 5 0 5 3]- reexported = Position 55 14 ; reexportedSig = getDocUri "Bar.hs" >>= \bar -> return [ExpectHoverText ["Bar", "Bar", "Haddock"], mkL bar 3 (if ghcVersion >= GHC94 then 5 else 0) 3 (if ghcVersion >= GHC94 then 8 else 14)]- thLocL57 = Position 59 10 ; thLoc = [ExpectHoverText ["Identity"]]- cmtL68 = Position 67 0 ; lackOfdEq = [ExpectHoverExcludeText ["$dEq"]]- in- mkFindTests- -- def hover look expect- [- if ghcVersion >= GHC90 then- -- It suggests either going to the constructor or to the field- test broken yes fffL4 fff "field in record definition"- else- test yes yes fffL4 fff "field in record definition"- , test yes yes fffL8 fff "field in record construction #1102"- , test yes yes fffL14 fff "field name used as accessor" -- https://github.com/haskell/ghcide/pull/120 in Calculate.hs- , test yes yes aaaL14 aaa "top-level name" -- https://github.com/haskell/ghcide/pull/120- , test yes yes dcL7 tcDC "data constructor record #1029"- , test yes yes dcL12 tcDC "data constructor plain" -- https://github.com/haskell/ghcide/pull/121- , test yes yes tcL6 tcData "type constructor #1028" -- https://github.com/haskell/ghcide/pull/147- , test broken yes xtcL5 xtc "type constructor external #717,1028"- , test broken yes xvL20 xvMsg "value external package #717" -- https://github.com/haskell/ghcide/pull/120- , test yes yes vvL16 vv "plain parameter" -- https://github.com/haskell/ghcide/pull/120- , test yes yes aL18 apmp "pattern match name" -- https://github.com/haskell/ghcide/pull/120- , test yes yes opL16 op "top-level operator #713" -- https://github.com/haskell/ghcide/pull/120- , test yes yes opL18 opp "parameter operator" -- https://github.com/haskell/ghcide/pull/120- , test yes yes b'L19 bp "name in backticks" -- https://github.com/haskell/ghcide/pull/120- , test yes yes clL23 cls "class in instance declaration #1027"- , test yes yes clL25 cls "class in signature #1027" -- https://github.com/haskell/ghcide/pull/147- , test broken yes eclL15 ecls "external class in signature #717,1027"- , test yes yes dnbL29 dnb "do-notation bind #1073"- , test yes yes dnbL30 dnb "do-notation lookup"- , test yes yes lcbL33 lcb "listcomp bind #1073"- , test yes yes lclL33 lcb "listcomp lookup"- , test yes yes mclL36 mcl "top-level fn 1st clause"- , test yes yes mclL37 mcl "top-level fn 2nd clause #1030"- , if ghcVersion >= GHC810 then- test yes yes spaceL37 space "top-level fn on space #1002"- else- test yes broken spaceL37 space "top-level fn on space #1002"- , test no yes docL41 doc "documentation #1129"- , test no yes eitL40 kindE "kind of Either #1017"- , test no yes intL40 kindI "kind of Int #1017"- , test no broken tvrL40 kindV "kind of (* -> *) type variable #1017"- , test no broken intL41 litI "literal Int in hover info #1016"- , test no broken chrL36 litC "literal Char in hover info #1016"- , test no broken txtL8 litT "literal Text in hover info #1016"- , test no broken lstL43 litL "literal List in hover info #1016"- , test yes yes cmtL68 lackOfdEq "no Core symbols #3280"- , if ghcVersion >= GHC90 then- test no yes docL41 constr "type constraint in hover info #1012"- else- test no broken docL41 constr "type constraint in hover info #1012"- , test no yes outL45 outSig "top-level signature #767"- , test broken broken innL48 innSig "inner signature #767"- , test no yes holeL60 hleInfo "hole without internal name #831"- , test no yes holeL65 hleInfo2 "hole with variable"- , test no yes cccL17 docLink "Haddock html links"- , testM yes yes imported importedSig "Imported symbol"- , if | isWindows ->- -- Flaky on Windows: https://github.com/haskell/haskell-language-server/issues/2997- testM no yes reexported reexportedSig "Imported symbol (reexported)"- | otherwise ->- testM yes yes reexported reexportedSig "Imported symbol (reexported)"- , if | ghcVersion == GHC90 && isWindows ->- test no broken thLocL57 thLoc "TH Splice Hover"- | otherwise ->- test no yes thLocL57 thLoc "TH Splice Hover"- ]- where yes, broken :: (TestTree -> Maybe TestTree)- yes = Just -- test should run and pass- broken = Just . (`xfail` "known broken")- no = const Nothing -- don't run this test at all- skip = const Nothing -- unreliable, don't run--checkFileCompiles :: FilePath -> Session () -> TestTree-checkFileCompiles fp diag =- testSessionWithExtraFiles "hover" ("Does " ++ fp ++ " compile") $ \dir -> do- void (openTestDataDoc (dir </> fp))- diag---pluginSimpleTests :: TestTree-pluginSimpleTests =- ignoreInWindowsForGHC810 $- -- Build profile: -w ghc-9.4.2 -O1- -- In order, the following will be built (use -v for more details):- -- - ghc-typelits-natnormalise-0.7.7 (lib) (requires build)- -- - ghc-typelits-knownnat-0.7.7 (lib) (requires build)- -- - plugin-1.0.0 (lib) (first run)- -- Starting ghc-typelits-natnormalise-0.7.7 (lib)- -- Building ghc-typelits-natnormalise-0.7.7 (lib)-- -- Failed to build ghc-typelits-natnormalise-0.7.7.- -- Build log (- -- C:\cabal\logs\ghc-9.4.2\ghc-typelits-_-0.7.7-3f036a52a0d9bfc3389d1852a87da2e87c6de2e4.log- -- ):- -- Preprocessing library for ghc-typelits-natnormalise-0.7.7..- -- Building library for ghc-typelits-natnormalise-0.7.7..- -- [1 of 3] Compiling GHC.TypeLits.Normalise.SOP ( src\GHC\TypeLits\Normalise\SOP.hs, dist\build\GHC\TypeLits\Normalise\SOP.o )- -- [2 of 3] Compiling GHC.TypeLits.Normalise.Unify ( src\GHC\TypeLits\Normalise\Unify.hs, dist\build\GHC\TypeLits\Normalise\Unify.o )- -- [3 of 3] Compiling GHC.TypeLits.Normalise ( src-ghc-9.4\GHC\TypeLits\Normalise.hs, dist\build\GHC\TypeLits\Normalise.o )- -- C:\tools\ghc-9.4.2\lib\../mingw/bin/llvm-ar.exe: error: dist\build\objs-5156\libHSghc-typelits-_-0.7.7-3f036a52a0d9bfc3389d1852a87da2e87c6de2e4.a: No such file or directory-- -- Error: cabal: Failed to build ghc-typelits-natnormalise-0.7.7 (which is- -- required by plugin-1.0.0). See the build log above for details.- ignoreFor (BrokenForGHC [GHC96]) "fragile, frequently times out" $- ignoreFor (BrokenSpecific Windows [GHC94]) "ghc-typelist-natnormalise fails to build on GHC 9.4.2 for windows only" $- testSessionWithExtraFiles "plugin-knownnat" "simple plugin" $ \dir -> do- _ <- openDoc (dir </> "KnownNat.hs") "haskell"- liftIO $ writeFile (dir</>"hie.yaml")- "cradle: {cabal: [{path: '.', component: 'lib:plugin'}]}"-- expectDiagnostics- [ ( "KnownNat.hs",- [(DsError, (9, 15), "Variable not in scope: c")]- )- ]--pluginParsedResultTests :: TestTree-pluginParsedResultTests =- ignoreInWindowsForGHC810 $- ignoreForGHC92Plus "No need for this plugin anymore!" $- testSessionWithExtraFiles "plugin-recorddot" "parsedResultAction plugin" $ \dir -> do- _ <- openDoc (dir</> "RecordDot.hs") "haskell"- expectNoMoreDiagnostics 2--cppTests :: TestTree-cppTests =- testGroup "cpp"- [ ignoreInWindowsBecause "Throw a lsp session time out in windows for ghc-8.8 and is broken for other versions" $ testCase "cpp-error" $ do- let content =- T.unlines- [ "{-# LANGUAGE CPP #-}",- "module Testing where",- "#ifdef FOO",- "foo = 42"- ]- -- The error locations differ depending on which C-preprocessor is used.- -- Some give the column number and others don't (hence maxBound == -1 unsigned). Assert either- -- of them.- (run $ expectError content (2, maxBound))- `catch` ( \e -> do- let _ = e :: HUnitFailure- run $ expectError content (2, 1)- )- , testSessionWait "cpp-ghcide" $ do- _ <- createDoc "A.hs" "haskell" $ T.unlines- ["{-# LANGUAGE CPP #-}"- ,"main ="- ,"#ifdef __GHCIDE__"- ," worked"- ,"#else"- ," failed"- ,"#endif"- ]- expectDiagnostics [("A.hs", [(DsError, (3, 2), "Variable not in scope: worked")])]- ]- where- expectError :: T.Text -> Cursor -> Session ()- expectError content cursor = do- _ <- createDoc "Testing.hs" "haskell" content- expectDiagnostics- [ ( "Testing.hs",- [(DsError, cursor, "error: unterminated")]- )- ]- expectNoMoreDiagnostics 0.5--preprocessorTests :: TestTree-preprocessorTests = testSessionWait "preprocessor" $ do- let content =- T.unlines- [ "{-# OPTIONS_GHC -F -pgmF=ghcide-test-preprocessor #-}"- , "module Testing where"- , "y = x + z" -- plugin replaces x with y, making this have only one diagnostic- ]- _ <- createDoc "Testing.hs" "haskell" content- expectDiagnostics- [ ( "Testing.hs",- [(DsError, (2, 8), "Variable not in scope: z")]- )- ]---safeTests :: TestTree-safeTests =- testGroup- "SafeHaskell"- [ -- Test for https://github.com/haskell/ghcide/issues/424- testSessionWait "load" $ do- let sourceA =- T.unlines- ["{-# LANGUAGE Trustworthy #-}"- ,"module A where"- ,"import System.IO.Unsafe"- ,"import System.IO ()"- ,"trustWorthyId :: a -> a"- ,"trustWorthyId i = unsafePerformIO $ do"- ," putStrLn \"I'm safe\""- ," return i"]- sourceB =- T.unlines- ["{-# LANGUAGE Safe #-}"- ,"module B where"- ,"import A"- ,"safeId :: a -> a"- ,"safeId = trustWorthyId"- ]-- _ <- createDoc "A.hs" "haskell" sourceA- _ <- createDoc "B.hs" "haskell" sourceB- expectNoMoreDiagnostics 1 ]--thTests :: TestTree-thTests =- testGroup- "TemplateHaskell"- [ -- Test for https://github.com/haskell/ghcide/pull/212- testSessionWait "load" $ do- let sourceA =- T.unlines- [ "{-# LANGUAGE PackageImports #-}",- "{-# LANGUAGE TemplateHaskell #-}",- "module A where",- "import \"template-haskell\" Language.Haskell.TH",- "a :: Integer",- "a = $(litE $ IntegerL 3)"- ]- sourceB =- T.unlines- [ "{-# LANGUAGE PackageImports #-}",- "{-# LANGUAGE TemplateHaskell #-}",- "module B where",- "import A",- "import \"template-haskell\" Language.Haskell.TH",- "b :: Integer",- "b = $(litE $ IntegerL $ a) + n"- ]- _ <- createDoc "A.hs" "haskell" sourceA- _ <- createDoc "B.hs" "haskell" sourceB- expectDiagnostics [ ( "B.hs", [(DsError, (6, 29), "Variable not in scope: n")] ) ]- , testSessionWait "newtype-closure" $ do- let sourceA =- T.unlines- [ "{-# LANGUAGE DeriveDataTypeable #-}"- ,"{-# LANGUAGE TemplateHaskell #-}"- ,"module A (a) where"- ,"import Data.Data"- ,"import Language.Haskell.TH"- ,"newtype A = A () deriving (Data)"- ,"a :: ExpQ"- ,"a = [| 0 |]"]- let sourceB =- T.unlines- [ "{-# LANGUAGE TemplateHaskell #-}"- ,"module B where"- ,"import A"- ,"b :: Int"- ,"b = $( a )" ]- _ <- createDoc "A.hs" "haskell" sourceA- _ <- createDoc "B.hs" "haskell" sourceB- return ()- , thReloadingTest False- , thLoadingTest- , thCoreTest- , ignoreInWindowsBecause "Broken in windows" $ thReloadingTest True- -- Regression test for https://github.com/haskell/haskell-language-server/issues/891- , thLinkingTest False- , ignoreInWindowsBecause "Broken in windows" $ thLinkingTest True- , testSessionWait "findsTHIdentifiers" $ do- let sourceA =- T.unlines- [ "{-# LANGUAGE TemplateHaskell #-}"- , "module A (a) where"- , "import Language.Haskell.TH (ExpQ)"- , "a :: ExpQ" -- TH 2.17 requires an explicit type signature since splices are polymorphic- , "a = [| glorifiedID |]"- , "glorifiedID :: a -> a"- , "glorifiedID = id" ]- let sourceB =- T.unlines- [ "{-# OPTIONS_GHC -Wall #-}"- , "{-# LANGUAGE TemplateHaskell #-}"- , "module B where"- , "import A"- , "main = $a (putStrLn \"success!\")"]- _ <- createDoc "A.hs" "haskell" sourceA- _ <- createDoc "B.hs" "haskell" sourceB- expectDiagnostics [ ( "B.hs", [(DsWarning, (4, 0), "Top-level binding with no type signature: main :: IO ()")] ) ]- , testCase "findsTHnewNameConstructor" $ runWithExtraFiles "THNewName" $ \dir -> do-- -- This test defines a TH value with the meaning "data A = A" in A.hs- -- Loads and export the template in B.hs- -- And checks wether the constructor A can be loaded in C.hs- -- This test does not fail when either A and B get manually loaded before C.hs- -- or when we remove the seemingly unnecessary TH pragma from C.hs-- let cPath = dir </> "C.hs"- _ <- openDoc cPath "haskell"- expectDiagnostics [ ( cPath, [(DsWarning, (3, 0), "Top-level binding with no type signature: a :: A")] ) ]- ]---- | Tests for projects that use symbolic links one way or another-symlinkTests :: TestTree-symlinkTests =- testGroup "Projects using Symlinks"- [ testCase "Module is symlinked" $ runWithExtraFiles "symlink" $ \dir -> do- liftIO $ createFileLink (dir </> "some_loc" </> "Sym.hs") (dir </> "other_loc" </> "Sym.hs")- let fooPath = dir </> "src" </> "Foo.hs"- _ <- openDoc fooPath "haskell"- expectDiagnosticsWithTags [("src" </> "Foo.hs", [(DsWarning, (2, 0), "The import of 'Sym' is redundant", Just DtUnnecessary)])]- pure ()- ]---- | Test that all modules have linkables-thLoadingTest :: TestTree-thLoadingTest = testCase "Loading linkables" $ runWithExtraFiles "THLoading" $ \dir -> do- let thb = dir </> "THB.hs"- _ <- openDoc thb "haskell"- expectNoMoreDiagnostics 1--thCoreTest :: TestTree-thCoreTest = testCase "Verifying TH core files" $ runWithExtraFiles "THCoreFile" $ \dir -> do- let thc = dir </> "THC.hs"- _ <- openDoc thc "haskell"- expectNoMoreDiagnostics 1---- | test that TH is reevaluated on typecheck-thReloadingTest :: Bool -> TestTree-thReloadingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do-- let aPath = dir </> "THA.hs"- bPath = dir </> "THB.hs"- cPath = dir </> "THC.hs"-- aSource <- liftIO $ readFileUtf8 aPath -- th = [d|a = ()|]- bSource <- liftIO $ readFileUtf8 bPath -- $th- cSource <- liftIO $ readFileUtf8 cPath -- c = a :: ()-- adoc <- createDoc aPath "haskell" aSource- bdoc <- createDoc bPath "haskell" bSource- cdoc <- createDoc cPath "haskell" cSource-- expectDiagnostics [("THB.hs", [(DsWarning, (4,thDollarIdx), "Top-level binding")])]-- -- Change th from () to Bool- let aSource' = T.unlines $ init (T.lines aSource) ++ ["th_a = [d| a = False|]"]- changeDoc adoc [TextDocumentContentChangeEvent Nothing Nothing aSource']- -- generate an artificial warning to avoid timing out if the TH change does not propagate- changeDoc cdoc [TextDocumentContentChangeEvent Nothing Nothing $ cSource <> "\nfoo=()"]-- -- Check that the change propagates to C- expectDiagnostics- [("THC.hs", [(DsError, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'")])- ,("THC.hs", [(DsWarning, (6,0), "Top-level binding")])- ,("THB.hs", [(DsWarning, (4,thDollarIdx), "Top-level bindin")])- ]-- closeDoc adoc- closeDoc bdoc- closeDoc cdoc- where- name = "reloading-th-test" <> if unboxed then "-unboxed" else ""- dir | unboxed = "THUnboxed"- | otherwise = "TH"--thLinkingTest :: Bool -> TestTree-thLinkingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do-- let aPath = dir </> "THA.hs"- bPath = dir </> "THB.hs"-- aSource <- liftIO $ readFileUtf8 aPath -- th_a = [d|a :: ()|]- bSource <- liftIO $ readFileUtf8 bPath -- $th_a-- adoc <- createDoc aPath "haskell" aSource- bdoc <- createDoc bPath "haskell" bSource-- expectDiagnostics [("THB.hs", [(DsWarning, (4,thDollarIdx), "Top-level binding")])]-- let aSource' = T.unlines $ init (init (T.lines aSource)) ++ ["th :: DecsQ", "th = [d| a = False|]"]- changeDoc adoc [TextDocumentContentChangeEvent Nothing Nothing aSource']-- -- modify b too- let bSource' = T.unlines $ init (T.lines bSource) ++ ["$th"]- changeDoc bdoc [TextDocumentContentChangeEvent Nothing Nothing bSource']- waitForProgressBegin- waitForAllProgressDone-- expectCurrentDiagnostics bdoc [(DsWarning, (4,thDollarIdx), "Top-level binding")]-- closeDoc adoc- closeDoc bdoc- where- name = "th-linking-test" <> if unboxed then "-unboxed" else ""- dir | unboxed = "THUnboxed"- | otherwise = "TH"--completionTests :: TestTree-completionTests- = testGroup "completion"- [- testGroup "non local" nonLocalCompletionTests- , testGroup "topLevel" topLevelCompletionTests- , testGroup "local" localCompletionTests- , testGroup "package" packageCompletionTests- , testGroup "project" projectCompletionTests- , testGroup "other" otherCompletionTests- , testGroup "doc" completionDocTests- ]--completionTest :: HasCallStack => String -> [T.Text] -> Position -> [(T.Text, CompletionItemKind, T.Text, Bool, Bool, Maybe (List TextEdit))] -> TestTree-completionTest name src pos expected = testSessionWait name $ do- docId <- createDoc "A.hs" "haskell" (T.unlines src)- _ <- waitForDiagnostics- compls <- getCompletions docId pos- let compls' = [ (_label, _kind, _insertText, _additionalTextEdits) | CompletionItem{..} <- compls]- let emptyToMaybe x = if T.null x then Nothing else Just x- liftIO $ sortOn (Lens.view Lens._1) (take (length expected) compls') @?=- sortOn (Lens.view Lens._1)- [ (l, Just k, emptyToMaybe t, at) | (l,k,t,_,_,at) <- expected]- forM_ (zip compls expected) $ \(item, (_,_,_,expectedSig, expectedDocs, _)) -> do- CompletionItem{..} <-- if expectedSig || expectedDocs- then do- rsp <- request SCompletionItemResolve item- case rsp ^. L.result of- Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err)- Right x -> pure x- else pure item- when expectedSig $- liftIO $ assertBool ("Missing type signature: " <> T.unpack _label) (isJust _detail)- when expectedDocs $- liftIO $ assertBool ("Missing docs: " <> T.unpack _label) (isJust _documentation)---topLevelCompletionTests :: [TestTree]-topLevelCompletionTests = [- completionTest- "variable"- ["bar = xx", "-- | haddock", "xxx :: ()", "xxx = ()", "-- | haddock", "data Xxx = XxxCon"]- (Position 0 8)- [("xxx", CiFunction, "xxx", True, True, Nothing)- ],- completionTest- "constructor"- ["bar = xx", "-- | haddock", "xxx :: ()", "xxx = ()", "-- | haddock", "data Xxx = XxxCon"]- (Position 0 8)- [("xxx", CiFunction, "xxx", True, True, Nothing)- ],- completionTest- "class method"- ["bar = xx", "class Xxx a where", "-- | haddock", "xxx :: ()", "xxx = ()"]- (Position 0 8)- [("xxx", CiFunction, "xxx", True, True, Nothing)],- completionTest- "type"- ["bar :: Xz", "zzz = ()", "-- | haddock", "data Xzz = XzzCon"]- (Position 0 9)- [("Xzz", CiStruct, "Xzz", False, True, Nothing)],- completionTest- "class"- ["bar :: Xz", "zzz = ()", "-- | haddock", "class Xzz a"]- (Position 0 9)- [("Xzz", CiInterface, "Xzz", False, True, Nothing)],- completionTest- "records"- ["data Person = Person { _personName:: String, _personAge:: Int}", "bar = Person { _pers }" ]- (Position 1 19)- [("_personName", CiFunction, "_personName", False, True, Nothing),- ("_personAge", CiFunction, "_personAge", False, True, Nothing)],- completionTest- "recordsConstructor"- ["data XxRecord = XyRecord { x:: String, y:: Int}", "bar = Xy" ]- (Position 1 19)- [("XyRecord", CiConstructor, "XyRecord", False, True, Nothing),- ("XyRecord", CiSnippet, "XyRecord {x=${1:_x}, y=${2:_y}}", False, True, Nothing)]- ]--localCompletionTests :: [TestTree]-localCompletionTests = [- completionTest- "argument"- ["bar (Just abcdef) abcdefg = abcd"]- (Position 0 32)- [("abcdef", CiFunction, "abcdef", True, False, Nothing),- ("abcdefg", CiFunction , "abcdefg", True, False, Nothing)- ],- completionTest- "let"- ["bar = let (Just abcdef) = undefined"- ," abcdefg = let abcd = undefined in undefined"- ," in abcd"- ]- (Position 2 15)- [("abcdef", CiFunction, "abcdef", True, False, Nothing),- ("abcdefg", CiFunction , "abcdefg", True, False, Nothing)- ],- completionTest- "where"- ["bar = abcd"- ," where (Just abcdef) = undefined"- ," abcdefg = let abcd = undefined in undefined"- ]- (Position 0 10)- [("abcdef", CiFunction, "abcdef", True, False, Nothing),- ("abcdefg", CiFunction , "abcdefg", True, False, Nothing)- ],- completionTest- "do/1"- ["bar = do"- ," Just abcdef <- undefined"- ," abcd"- ," abcdefg <- undefined"- ," pure ()"- ]- (Position 2 6)- [("abcdef", CiFunction, "abcdef", True, False, Nothing)- ],- completionTest- "do/2"- ["bar abcde = do"- ," Just [(abcdef,_)] <- undefined"- ," abcdefg <- undefined"- ," let abcdefgh = undefined"- ," (Just [abcdefghi]) = undefined"- ," abcd"- ," where"- ," abcdefghij = undefined"- ]- (Position 5 8)- [("abcde", CiFunction, "abcde", True, False, Nothing)- ,("abcdefghij", CiFunction, "abcdefghij", True, False, Nothing)- ,("abcdef", CiFunction, "abcdef", True, False, Nothing)- ,("abcdefg", CiFunction, "abcdefg", True, False, Nothing)- ,("abcdefgh", CiFunction, "abcdefgh", True, False, Nothing)- ,("abcdefghi", CiFunction, "abcdefghi", True, False, Nothing)- ],- completionTest- "type family"- ["{-# LANGUAGE DataKinds, TypeFamilies #-}"- ,"type family Bar a"- ,"a :: Ba"- ]- (Position 2 7)- [("Bar", CiStruct, "Bar", True, False, Nothing)- ],- completionTest- "class method"- [- "class Test a where"- , " abcd :: a -> ()"- , " abcde :: a -> Int"- , "instance Test Int where"- , " abcd = abc"- ]- (Position 4 14)- [("abcd", CiFunction, "abcd", True, False, Nothing)- ,("abcde", CiFunction, "abcde", True, False, Nothing)- ],- testSessionWait "incomplete entries" $ do- let src a = "data Data = " <> a- doc <- createDoc "A.hs" "haskell" $ src "AAA"- void $ waitForTypecheck doc- let editA rhs =- changeDoc doc [TextDocumentContentChangeEvent- { _range=Nothing- , _rangeLength=Nothing- , _text=src rhs}]-- editA "AAAA"- void $ waitForTypecheck doc- editA "AAAAA"- void $ waitForTypecheck doc-- compls <- getCompletions doc (Position 0 15)- liftIO $ filter ("AAA" `T.isPrefixOf`) (mapMaybe _insertText compls) @?= ["AAAAA"]- pure ()- ]--nonLocalCompletionTests :: [TestTree]-nonLocalCompletionTests =- [ brokenForWinGhc $ completionTest- "variable"- ["module A where", "f = hea"]- (Position 1 7)- [("head", CiFunction, "head", True, True, Nothing)],- completionTest- "constructor"- ["{-# OPTIONS_GHC -Wall #-}", "module A where", "f = True"]- (Position 2 8)- [ ("True", CiConstructor, "True", True, True, Nothing)- ],- brokenForWinGhc $ completionTest- "type"- ["{-# OPTIONS_GHC -Wall #-}", "module A () where", "f :: Boo", "f = True"]- (Position 2 8)- [ ("Bool", CiStruct, "Bool", True, True, Nothing)- ],- completionTest- "qualified"- ["{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", "f = Prelude.hea"]- (Position 2 15)- [ ("head", CiFunction, "head", True, True, Nothing)- ],- completionTest- "duplicate import"- ["module A where", "import Data.List", "import Data.List", "f = permu"]- (Position 3 9)- [ ("permutations", CiFunction, "permutations", False, False, Nothing)- ],- completionTest- "dont show hidden items"- [ "{-# LANGUAGE NoImplicitPrelude #-}",- "module A where",- "import Control.Monad hiding (join)",- "f = joi"- ]- (Position 3 6)- [],- testGroup "ordering"- [completionTest "qualified has priority"- ["module A where"- ,"import qualified Data.ByteString as BS"- ,"f = BS.read"- ]- (Position 2 10)- [("readFile", CiFunction, "readFile", True, True, Nothing)]- ],- -- we need this test to make sure the ghcide completions module does not return completions for language pragmas. this functionality is turned on in hls- completionTest- "do not show pragma completions"- [ "{-# LANGUAGE ",- "{module A where}",- "main = return ()"- ]- (Position 0 13)- []- ]- where- brokenForWinGhc = knownBrokenFor (BrokenSpecific Windows [GHC810, GHC90, GHC92, GHC94, GHC96]) "Windows has strange things in scope for some reason"--otherCompletionTests :: [TestTree]-otherCompletionTests = [- completionTest- "keyword"- ["module A where", "f = newty"]- (Position 1 9)- [("newtype", CiKeyword, "", False, False, Nothing)],- completionTest- "type context"- [ "{-# OPTIONS_GHC -Wunused-binds #-}",- "module A () where",- "f = f",- "g :: Intege"- ]- -- At this point the module parses but does not typecheck.- -- This should be sufficient to detect that we are in a- -- type context and only show the completion to the type.- (Position 3 11)- [("Integer", CiStruct, "Integer", True, True, Nothing)],-- testSession "duplicate record fields" $ do- void $- createDoc "B.hs" "haskell" $- T.unlines- [ "{-# LANGUAGE DuplicateRecordFields #-}",- "module B where",- "newtype Foo = Foo { member :: () }",- "newtype Bar = Bar { member :: () }"- ]- docA <-- createDoc "A.hs" "haskell" $- T.unlines- [ "module A where",- "import B",- "memb"- ]- _ <- waitForDiagnostics- compls <- getCompletions docA $ Position 2 4- let compls' = [txt | CompletionItem {_insertText = Just txt, ..} <- compls, _label == "member"]- liftIO $ take 2 compls' @?= ["member"],-- testSessionWait "maxCompletions" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS_GHC -Wunused-binds #-}",- "module A () where",- "a = Prelude."- ]- _ <- waitForDiagnostics- compls <- getCompletions doc (Position 3 13)- liftIO $ length compls @?= maxCompletions def- ]--packageCompletionTests :: [TestTree]-packageCompletionTests =- [ testSession' "fromList" $ \dir -> do- liftIO $ writeFile (dir </> "hie.yaml")- "cradle: {direct: {arguments: [-hide-all-packages, -package, base, A]}}"- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS_GHC -Wunused-binds #-}",- "module A () where",- "a = fromList"- ]- _ <- waitForDiagnostics- compls <- getCompletions doc (Position 2 12)- let compls' =- [T.drop 1 $ T.dropEnd 3 d- | CompletionItem {_documentation = Just (CompletionDocMarkup (MarkupContent MkMarkdown d)), _label}- <- compls- , _label == "fromList"- ]- liftIO $ take 3 (sort compls') @?=- map ("Defined in "<>) (- [ "'Data.List.NonEmpty"- , "'GHC.Exts"- ] ++ if ghcVersion >= GHC94 then [ "'GHC.IsList" ] else [])-- , testSessionWait "Map" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS_GHC -Wunused-binds #-}",- "module A () where",- "a :: Map"- ]- _ <- waitForDiagnostics- compls <- getCompletions doc (Position 2 7)- let compls' =- [T.drop 1 $ T.dropEnd 3 d- | CompletionItem {_documentation = Just (CompletionDocMarkup (MarkupContent MkMarkdown d)), _label}- <- compls- , _label == "Map"- ]- liftIO $ take 3 (sort compls') @?=- map ("Defined in "<>)- [ "'Data.Map"- , "'Data.Map.Lazy"- , "'Data.Map.Strict"- ]- , testSessionWait "no duplicates" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS_GHC -Wunused-binds #-}",- "module A () where",- "import GHC.Exts(fromList)",- "a = fromList"- ]- _ <- waitForDiagnostics- compls <- getCompletions doc (Position 3 13)- let duplicate =- filter- (\case- CompletionItem- { _insertText = Just "fromList"- , _documentation =- Just (CompletionDocMarkup (MarkupContent MkMarkdown d))- } ->- "GHC.Exts" `T.isInfixOf` d- _ -> False- ) compls- liftIO $ length duplicate @?= 1-- , testSessionWait "non-local before global" $ do- -- non local completions are more specific- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS_GHC -Wunused-binds #-}",- "module A () where",- "import GHC.Exts(fromList)",- "a = fromList"- ]- _ <- waitForDiagnostics- compls <- getCompletions doc (Position 3 13)- let compls' =- [_insertText- | CompletionItem {_label, _insertText} <- compls- , _label == "fromList"- ]- liftIO $ take 3 compls' @?=- map Just ["fromList"]- ]--projectCompletionTests :: [TestTree]-projectCompletionTests =- [ testSession' "from hiedb" $ \dir-> do- liftIO $ writeFile (dir </> "hie.yaml")- "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"- _ <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A (anidentifier) where",- "anidentifier = ()"- ]- _ <- waitForDiagnostics- -- Note that B does not import A- doc <- createDoc "B.hs" "haskell" $ T.unlines- [ "module B where",- "b = anidenti"- ]- compls <- getCompletions doc (Position 1 10)- let compls' =- [T.drop 1 $ T.dropEnd 3 d- | CompletionItem {_documentation = Just (CompletionDocMarkup (MarkupContent MkMarkdown d)), _label}- <- compls- , _label == "anidentifier"- ]- liftIO $ compls' @?= ["Defined in 'A"],- testSession' "auto complete project imports" $ \dir-> do- liftIO $ writeFile (dir </> "hie.yaml")- "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"ALocalModule\", \"B\"]}}"- _ <- createDoc "ALocalModule.hs" "haskell" $ T.unlines- [ "module ALocalModule (anidentifier) where",- "anidentifier = ()"- ]- _ <- waitForDiagnostics- -- Note that B does not import A- doc <- createDoc "B.hs" "haskell" $ T.unlines- [ "module B where",- "import ALocal"- ]- compls <- getCompletions doc (Position 1 13)- let item = head $ filter ((== "ALocalModule") . (^. Lens.label)) compls- liftIO $ do- item ^. Lens.label @?= "ALocalModule",- testSession' "auto complete functions from qualified imports without alias" $ \dir-> do- liftIO $ writeFile (dir </> "hie.yaml")- "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"- _ <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A (anidentifier) where",- "anidentifier = ()"- ]- _ <- waitForDiagnostics- doc <- createDoc "B.hs" "haskell" $ T.unlines- [ "module B where",- "import qualified A",- "A."- ]- compls <- getCompletions doc (Position 2 2)- let item = head compls- liftIO $ do- item ^. L.label @?= "anidentifier",- testSession' "auto complete functions from qualified imports with alias" $ \dir-> do- liftIO $ writeFile (dir </> "hie.yaml")- "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"- _ <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A (anidentifier) where",- "anidentifier = ()"- ]- _ <- waitForDiagnostics- doc <- createDoc "B.hs" "haskell" $ T.unlines- [ "module B where",- "import qualified A as Alias",- "foo = Alias."- ]- compls <- getCompletions doc (Position 2 12)- let item = head compls- liftIO $ do- item ^. L.label @?= "anidentifier"- ]--completionDocTests :: [TestTree]-completionDocTests =- [ testSession "local define" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "foo = ()"- , "bar = fo"- ]- let expected = "*Defined at line 2, column 1 in this module*\n"- test doc (Position 2 8) "foo" Nothing [expected]- , testSession "local empty doc" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "foo = ()"- , "bar = fo"- ]- test doc (Position 2 8) "foo" Nothing ["*Defined at line 2, column 1 in this module*\n"]- , testSession "local single line doc without newline" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "-- |docdoc"- , "foo = ()"- , "bar = fo"- ]- test doc (Position 3 8) "foo" Nothing ["*Defined at line 3, column 1 in this module*\n* * *\n\n\ndocdoc\n"]- , testSession "local multi line doc with newline" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "-- | abcabc"- , "--"- , "foo = ()"- , "bar = fo"- ]- test doc (Position 4 8) "foo" Nothing ["*Defined at line 4, column 1 in this module*\n* * *\n\n\nabcabc\n"]- , testSession "local multi line doc without newline" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "-- | abcabc"- , "--"- , "--def"- , "foo = ()"- , "bar = fo"- ]- test doc (Position 5 8) "foo" Nothing ["*Defined at line 5, column 1 in this module*\n* * *\n\n\nabcabc \n\ndef\n"]- , testSession "extern empty doc" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "foo = od"- ]- let expected = "*Imported from 'Prelude'*\n"- test doc (Position 1 8) "odd" (Just $ T.length expected) [expected]- , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern single line doc without '\\n'" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "foo = no"- ]- let expected = "*Imported from 'Prelude'*\n* * *\n\n\nBoolean \"not\"\n"- test doc (Position 1 8) "not" (Just $ T.length expected) [expected]- , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern mulit line doc" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "foo = i"- ]- let expected = "*Imported from 'Prelude'*\n* * *\n\n\nIdentity function. \n```haskell\nid x = x\n```\n"- test doc (Position 1 7) "id" (Just $ T.length expected) [expected]- , testSession "extern defined doc" $ do- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "module A where"- , "foo = i"- ]- let expected = "*Imported from 'Prelude'*\n"- test doc (Position 1 7) "id" (Just $ T.length expected) [expected]- ]- where- brokenForGhc9 = knownBrokenFor (BrokenForGHC [GHC90, GHC92, GHC94, GHC96]) "Completion doc doesn't support ghc9"- brokenForWinGhc90 = knownBrokenFor (BrokenSpecific Windows [GHC90]) "Extern doc doesn't support Windows for ghc9.2"- -- https://gitlab.haskell.org/ghc/ghc/-/issues/20903- brokenForMacGhc9 = knownBrokenFor (BrokenSpecific MacOS [GHC90, GHC92, GHC94, GHC96]) "Extern doc doesn't support MacOS for ghc9"- test doc pos label mn expected = do- _ <- waitForDiagnostics- compls <- getCompletions doc pos- rcompls <- forM compls $ \item -> do- rsp <- request SCompletionItemResolve item- case rsp ^. L.result of- Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err)- Right x -> pure x- let compls' = [- -- We ignore doc uris since it points to the local path which determined by specific machines- case mn of- Nothing -> txt- Just n -> T.take n txt- | CompletionItem {_documentation = Just (CompletionDocMarkup (MarkupContent MkMarkdown txt)), ..} <- rcompls- , _label == label- ]- liftIO $ compls' @?= expected--highlightTests :: TestTree-highlightTests = testGroup "highlight"- [ testSessionWait "value" $ do- doc <- createDoc "A.hs" "haskell" source- _ <- waitForDiagnostics- highlights <- getHighlights doc (Position 3 2)- liftIO $ highlights @?= List- [ DocumentHighlight (R 2 0 2 3) (Just HkRead)- , DocumentHighlight (R 3 0 3 3) (Just HkWrite)- , DocumentHighlight (R 4 6 4 9) (Just HkRead)- , DocumentHighlight (R 5 22 5 25) (Just HkRead)- ]- , testSessionWait "type" $ do- doc <- createDoc "A.hs" "haskell" source- _ <- waitForDiagnostics- highlights <- getHighlights doc (Position 2 8)- liftIO $ highlights @?= List- [ DocumentHighlight (R 2 7 2 10) (Just HkRead)- , DocumentHighlight (R 3 11 3 14) (Just HkRead)- ]- , testSessionWait "local" $ do- doc <- createDoc "A.hs" "haskell" source- _ <- waitForDiagnostics- highlights <- getHighlights doc (Position 6 5)- liftIO $ highlights @?= List- [ DocumentHighlight (R 6 4 6 7) (Just HkWrite)- , DocumentHighlight (R 6 10 6 13) (Just HkRead)- , DocumentHighlight (R 7 12 7 15) (Just HkRead)- ]- , knownBrokenForGhcVersions [GHC90, GHC92, GHC94, GHC96] "Ghc9 highlights the constructor and not just this field" $- testSessionWait "record" $ do- doc <- createDoc "A.hs" "haskell" recsource- _ <- waitForDiagnostics- highlights <- getHighlights doc (Position 4 15)- liftIO $ highlights @?= List- -- Span is just the .. on 8.10, but Rec{..} before- [ if ghcVersion >= GHC810- then DocumentHighlight (R 4 8 4 10) (Just HkWrite)- else DocumentHighlight (R 4 4 4 11) (Just HkWrite)- , DocumentHighlight (R 4 14 4 20) (Just HkRead)- ]- highlights <- getHighlights doc (Position 3 17)- liftIO $ highlights @?= List- [ DocumentHighlight (R 3 17 3 23) (Just HkWrite)- -- Span is just the .. on 8.10, but Rec{..} before- , if ghcVersion >= GHC810- then DocumentHighlight (R 4 8 4 10) (Just HkRead)- else DocumentHighlight (R 4 4 4 11) (Just HkRead)- ]- ]- where- source = T.unlines- ["{-# OPTIONS_GHC -Wunused-binds #-}"- ,"module Highlight () where"- ,"foo :: Int"- ,"foo = 3 :: Int"- ,"bar = foo"- ," where baz = let x = foo in x"- ,"baz arg = arg + x"- ," where x = arg"- ]- recsource = T.unlines- ["{-# LANGUAGE RecordWildCards #-}"- ,"{-# OPTIONS_GHC -Wunused-binds #-}"- ,"module Highlight () where"- ,"data Rec = Rec { field1 :: Int, field2 :: Char }"- ,"foo Rec{..} = field2 + field1"- ]--outlineTests :: TestTree-outlineTests = testGroup- "outline"- [ testSessionWait "type class" $ do- let source = T.unlines ["module A where", "class A a where a :: a -> Bool"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [ moduleSymbol- "A"- (R 0 7 0 8)- [ classSymbol "A a"- (R 1 0 1 30)- [docSymbol' "a" SkMethod (R 1 16 1 30) (R 1 16 1 17)]- ]- ]- , testSessionWait "type class instance " $ do- let source = T.unlines ["class A a where", "instance A () where"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [ classSymbol "A a" (R 0 0 0 15) []- , docSymbol "A ()" SkInterface (R 1 0 1 19)- ]- , testSessionWait "type family" $ do- let source = T.unlines ["{-# language TypeFamilies #-}", "type family A"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left [docSymbolD "A" "type family" SkFunction (R 1 0 1 13)]- , testSessionWait "type family instance " $ do- let source = T.unlines- [ "{-# language TypeFamilies #-}"- , "type family A a"- , "type instance A () = ()"- ]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [ docSymbolD "A a" "type family" SkFunction (R 1 0 1 15)- , docSymbol "A ()" SkInterface (R 2 0 2 23)- ]- , testSessionWait "data family" $ do- let source = T.unlines ["{-# language TypeFamilies #-}", "data family A"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left [docSymbolD "A" "data family" SkFunction (R 1 0 1 11)]- , testSessionWait "data family instance " $ do- let source = T.unlines- [ "{-# language TypeFamilies #-}"- , "data family A a"- , "data instance A () = A ()"- ]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [ docSymbolD "A a" "data family" SkFunction (R 1 0 1 11)- , docSymbol "A ()" SkInterface (R 2 0 2 25)- ]- , testSessionWait "constant" $ do- let source = T.unlines ["a = ()"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [docSymbol "a" SkFunction (R 0 0 0 6)]- , testSessionWait "pattern" $ do- let source = T.unlines ["Just foo = Just 21"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [docSymbol "Just foo" SkFunction (R 0 0 0 18)]- , testSessionWait "pattern with type signature" $ do- let source = T.unlines ["{-# language ScopedTypeVariables #-}", "a :: () = ()"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [docSymbol "a :: ()" SkFunction (R 1 0 1 12)]- , testSessionWait "function" $ do- let source = T.unlines ["a _x = ()"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left [docSymbol "a" SkFunction (R 0 0 0 9)]- , testSessionWait "type synonym" $ do- let source = T.unlines ["type A = Bool"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [docSymbol' "A" SkTypeParameter (R 0 0 0 13) (R 0 5 0 6)]- , testSessionWait "datatype" $ do- let source = T.unlines ["data A = C"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [ docSymbolWithChildren "A"- SkStruct- (R 0 0 0 10)- [docSymbol "C" SkConstructor (R 0 9 0 10)]- ]- , testSessionWait "record fields" $ do- let source = T.unlines ["data A = B {", " x :: Int", " , y :: Int}"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [ docSymbolWithChildren "A" SkStruct (R 0 0 2 13)- [ docSymbolWithChildren' "B" SkConstructor (R 0 9 2 13) (R 0 9 0 10)- [ docSymbol "x" SkField (R 1 2 1 3)- , docSymbol "y" SkField (R 2 4 2 5)- ]- ]- ]- , testSessionWait "import" $ do- let source = T.unlines ["import Data.Maybe ()"]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [docSymbolWithChildren "imports"- SkModule- (R 0 0 0 20)- [ docSymbol "import Data.Maybe" SkModule (R 0 0 0 20)- ]- ]- , testSessionWait "multiple import" $ do- let source = T.unlines ["", "import Data.Maybe ()", "", "import Control.Exception ()", ""]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left- [docSymbolWithChildren "imports"- SkModule- (R 1 0 3 27)- [ docSymbol "import Data.Maybe" SkModule (R 1 0 1 20)- , docSymbol "import Control.Exception" SkModule (R 3 0 3 27)- ]- ]- , testSessionWait "foreign import" $ do- let source = T.unlines- [ "{-# language ForeignFunctionInterface #-}"- , "foreign import ccall \"a\" a :: Int"- ]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left [docSymbolD "a" "import" SkObject (R 1 0 1 33)]- , testSessionWait "foreign export" $ do- let source = T.unlines- [ "{-# language ForeignFunctionInterface #-}"- , "foreign export ccall odd :: Int -> Bool"- ]- docId <- createDoc "A.hs" "haskell" source- symbols <- getDocumentSymbols docId- liftIO $ symbols @?= Left [docSymbolD "odd" "export" SkObject (R 1 0 1 39)]- ]- where- docSymbol name kind loc =- DocumentSymbol name Nothing kind Nothing Nothing loc loc Nothing- docSymbol' name kind loc selectionLoc =- DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc Nothing- docSymbolD name detail kind loc =- DocumentSymbol name (Just detail) kind Nothing Nothing loc loc Nothing- docSymbolWithChildren name kind loc cc =- DocumentSymbol name Nothing kind Nothing Nothing loc loc (Just $ List cc)- docSymbolWithChildren' name kind loc selectionLoc cc =- DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc (Just $ List cc)- moduleSymbol name loc cc = DocumentSymbol name- Nothing- SkFile- Nothing- Nothing- (R 0 0 maxBound 0)- loc- (Just $ List cc)- classSymbol name loc cc = DocumentSymbol name- (Just "class")- SkInterface- Nothing- Nothing- loc- loc- (Just $ List cc)--pattern R :: UInt -> UInt -> UInt -> UInt -> Range-pattern R x y x' y' = Range (Position x y) (Position x' y')--xfail :: TestTree -> String -> TestTree-xfail = flip expectFailBecause--ignoreInWindowsBecause :: String -> TestTree -> TestTree-ignoreInWindowsBecause = ignoreFor (BrokenForOS Windows)--ignoreInWindowsForGHC810 :: TestTree -> TestTree-ignoreInWindowsForGHC810 =- ignoreFor (BrokenSpecific Windows [GHC810]) "tests are unreliable in windows for ghc 8.10"--ignoreForGHC92Plus :: String -> TestTree -> TestTree-ignoreForGHC92Plus = ignoreFor (BrokenForGHC [GHC92, GHC94, GHC96])--knownBrokenForGhcVersions :: [GhcVersion] -> String -> TestTree -> TestTree-knownBrokenForGhcVersions ghcVers = knownBrokenFor (BrokenForGHC ghcVers)--data BrokenOS = Linux | MacOS | Windows deriving (Show)--data IssueSolution = Broken | Ignore deriving (Show)--data BrokenTarget =- BrokenSpecific BrokenOS [GhcVersion]- -- ^Broken for `BrokenOS` with `GhcVersion`- | BrokenForOS BrokenOS- -- ^Broken for `BrokenOS`- | BrokenForGHC [GhcVersion]- -- ^Broken for `GhcVersion`- deriving (Show)---- | Ignore test for specific os and ghc with reason.-ignoreFor :: BrokenTarget -> String -> TestTree -> TestTree-ignoreFor = knownIssueFor Ignore---- | Known broken for specific os and ghc with reason.-knownBrokenFor :: BrokenTarget -> String -> TestTree -> TestTree-knownBrokenFor = knownIssueFor Broken---- | Deal with `IssueSolution` for specific OS and GHC.-knownIssueFor :: IssueSolution -> BrokenTarget -> String -> TestTree -> TestTree-knownIssueFor solution = go . \case- BrokenSpecific bos vers -> isTargetOS bos && isTargetGhc vers- BrokenForOS bos -> isTargetOS bos- BrokenForGHC vers -> isTargetGhc vers- where- isTargetOS = \case- Windows -> isWindows- MacOS -> isMac- Linux -> not isWindows && not isMac-- isTargetGhc = elem ghcVersion-- go True = case solution of- Broken -> expectFailBecause- Ignore -> ignoreTestBecause- go False = \_ -> id--data Expect- = ExpectRange Range -- Both gotoDef and hover should report this range- | ExpectLocation Location--- | ExpectDefRange Range -- Only gotoDef should report this range- | ExpectHoverRange Range -- Only hover should report this range- | ExpectHoverText [T.Text] -- the hover message must contain these snippets- | ExpectHoverExcludeText [T.Text] -- the hover message must _not_ contain these snippets- | ExpectHoverTextRegex T.Text -- the hover message must match this pattern- | ExpectExternFail -- definition lookup in other file expected to fail- | ExpectNoDefinitions- | ExpectNoHover--- | ExpectExtern -- TODO: as above, but expected to succeed: need some more info in here, once we have some working examples- deriving Eq--mkR :: UInt -> UInt -> UInt -> UInt -> Expect-mkR startLine startColumn endLine endColumn = ExpectRange $ mkRange startLine startColumn endLine endColumn--mkL :: Uri -> UInt -> UInt -> UInt -> UInt -> Expect-mkL uri startLine startColumn endLine endColumn = ExpectLocation $ Location uri $ mkRange startLine startColumn endLine endColumn--haddockTests :: TestTree-haddockTests- = testGroup "haddock"- [ testCase "Num" $ checkHaddock- (unlines- [ "However, '(+)' and '(*)' are"- , "customarily expected to define a ring and have the following properties:"- , ""- , "[__Associativity of (+)__]: @(x + y) + z@ = @x + (y + z)@"- , "[__Commutativity of (+)__]: @x + y@ = @y + x@"- , "[__@fromInteger 0@ is the additive identity__]: @x + fromInteger 0@ = @x@"- ]- )- (unlines- [ ""- , ""- , "However, `(+)` and `(*)` are"- , "customarily expected to define a ring and have the following properties: "- , "+ ****Associativity of (+)****: `(x + y) + z` = `x + (y + z)`"- , "+ ****Commutativity of (+)****: `x + y` = `y + x`"- , "+ ****`fromInteger 0` is the additive identity****: `x + fromInteger 0` = `x`"- ]- )- , testCase "unsafePerformIO" $ checkHaddock- (unlines- [ "may require"- , "different precautions:"- , ""- , " * Use @{\\-\\# NOINLINE foo \\#-\\}@ as a pragma on any function @foo@"- , " that calls 'unsafePerformIO'. If the call is inlined,"- , " the I\\/O may be performed more than once."- , ""- , " * Use the compiler flag @-fno-cse@ to prevent common sub-expression"- , " elimination being performed on the module."- , ""- ]- )- (unlines- [ ""- , ""- , "may require"- , "different precautions: "- , "+ Use `{-# NOINLINE foo #-}` as a pragma on any function `foo` "- , " that calls `unsafePerformIO` . If the call is inlined,"- , " the I/O may be performed more than once."- , ""- , "+ Use the compiler flag `-fno-cse` to prevent common sub-expression"- , " elimination being performed on the module."- , ""- ]- )- , testCase "ordered list" $ checkHaddock- (unlines- [ "may require"- , "different precautions:"- , ""- , " 1. Use @{\\-\\# NOINLINE foo \\#-\\}@ as a pragma on any function @foo@"- , " that calls 'unsafePerformIO'. If the call is inlined,"- , " the I\\/O may be performed more than once."- , ""- , " 2. Use the compiler flag @-fno-cse@ to prevent common sub-expression"- , " elimination being performed on the module."- , ""- ]- )- (unlines- [ ""- , ""- , "may require"- , "different precautions: "- , "1. Use `{-# NOINLINE foo #-}` as a pragma on any function `foo` "- , " that calls `unsafePerformIO` . If the call is inlined,"- , " the I/O may be performed more than once."- , ""- , "2. Use the compiler flag `-fno-cse` to prevent common sub-expression"- , " elimination being performed on the module."- , ""- ]- )- ]- where- checkHaddock s txt = spanDocToMarkdownForTest s @?= txt--cradleTests :: TestTree-cradleTests = testGroup "cradle"- [testGroup "dependencies" [sessionDepsArePickedUp]- ,testGroup "ignore-fatal" [ignoreFatalWarning]- ,testGroup "loading" [loadCradleOnlyonce, retryFailedCradle]- ,testGroup "multi" [simpleMultiTest, simpleMultiTest2, simpleMultiTest3, simpleMultiDefTest]- ,testGroup "sub-directory" [simpleSubDirectoryTest]- ]--loadCradleOnlyonce :: TestTree-loadCradleOnlyonce = testGroup "load cradle only once"- [ testSession' "implicit" implicit- , testSession' "direct" direct- ]- where- direct dir = do- liftIO $ writeFileUTF8 (dir </> "hie.yaml")- "cradle: {direct: {arguments: []}}"- test dir- implicit dir = test dir- test _dir = do- doc <- createDoc "B.hs" "haskell" "module B where\nimport Data.Foo"- msgs <- someTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message STextDocumentPublishDiagnostics))- liftIO $ length msgs @?= 1- changeDoc doc [TextDocumentContentChangeEvent Nothing Nothing "module B where\nimport Data.Maybe"]- msgs <- manyTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message STextDocumentPublishDiagnostics))- liftIO $ length msgs @?= 0- _ <- createDoc "A.hs" "haskell" "module A where\nimport LoadCradleBar"- msgs <- manyTill (skipManyTill anyMessage cradleLoadedMessage) (skipManyTill anyMessage (message STextDocumentPublishDiagnostics))- liftIO $ length msgs @?= 0--retryFailedCradle :: TestTree-retryFailedCradle = testSession' "retry failed" $ \dir -> do- -- The false cradle always fails- let hieContents = "cradle: {bios: {shell: \"false\"}}"- hiePath = dir </> "hie.yaml"- liftIO $ writeFile hiePath hieContents- let aPath = dir </> "A.hs"- doc <- createDoc aPath "haskell" "main = return ()"- WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc- liftIO $ "Test assumption failed: cradle should error out" `assertBool` not ideResultSuccess-- -- Fix the cradle and typecheck again- let validCradle = "cradle: {bios: {shell: \"echo A.hs\"}}"- liftIO $ writeFileUTF8 hiePath $ T.unpack validCradle- sendNotification SWorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $- List [FileEvent (filePathToUri $ dir </> "hie.yaml") FcChanged ]-- WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" doc- liftIO $ "No joy after fixing the cradle" `assertBool` ideResultSuccess---dependentFileTest :: TestTree-dependentFileTest = testGroup "addDependentFile"- [testGroup "file-changed" [testSession' "test" test]- ]- where- test dir = do- -- If the file contains B then no type error- -- otherwise type error- let depFilePath = dir </> "dep-file.txt"- liftIO $ writeFile depFilePath "A"- let fooContent = T.unlines- [ "{-# LANGUAGE TemplateHaskell #-}"- , "module Foo where"- , "import Language.Haskell.TH.Syntax"- , "foo :: Int"- , "foo = 1 + $(do"- , " qAddDependentFile \"dep-file.txt\""- , " f <- qRunIO (readFile \"dep-file.txt\")"- , " if f == \"B\" then [| 1 |] else lift f)"- ]- let bazContent = T.unlines ["module Baz where", "import Foo ()"]- _ <- createDoc "Foo.hs" "haskell" fooContent- doc <- createDoc "Baz.hs" "haskell" bazContent- expectDiagnostics $- if ghcVersion >= GHC90- -- String vs [Char] causes this change in error message- then [("Foo.hs", [(DsError, if ghcVersion >= GHC92 then (4,11) else (4, 6), "Couldn't match type")])]- else [("Foo.hs", [(DsError, (4, 6), "Couldn't match expected type")])]- -- Now modify the dependent file- liftIO $ writeFile depFilePath "B"- sendNotification SWorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $- List [FileEvent (filePathToUri "dep-file.txt") FcChanged ]-- -- Modifying Baz will now trigger Foo to be rebuilt as well- let change = TextDocumentContentChangeEvent- { _range = Just (Range (Position 2 0) (Position 2 6))- , _rangeLength = Nothing- , _text = "f = ()"- }- changeDoc doc [change]- expectDiagnostics [("Foo.hs", [])]---cradleLoadedMessage :: Session FromServerMessage-cradleLoadedMessage = satisfy $ \case- FromServerMess (SCustomMethod m) (NotMess _) -> m == cradleLoadedMethod- _ -> False--cradleLoadedMethod :: T.Text-cradleLoadedMethod = "ghcide/cradle/loaded"--ignoreFatalWarning :: TestTree-ignoreFatalWarning = testCase "ignore-fatal-warning" $ runWithExtraFiles "ignore-fatal" $ \dir -> do- let srcPath = dir </> "IgnoreFatal.hs"- src <- liftIO $ readFileUtf8 srcPath- _ <- createDoc srcPath "haskell" src- expectNoMoreDiagnostics 5--simpleSubDirectoryTest :: TestTree-simpleSubDirectoryTest =- testCase "simple-subdirectory" $ runWithExtraFiles "cabal-exe" $ \dir -> do- let mainPath = dir </> "a/src/Main.hs"- mainSource <- liftIO $ readFileUtf8 mainPath- _mdoc <- createDoc mainPath "haskell" mainSource- expectDiagnosticsWithTags- [("a/src/Main.hs", [(DsWarning,(2,0), "Top-level binding", Nothing)]) -- So that we know P has been loaded- ]- expectNoMoreDiagnostics 0.5--simpleMultiTest :: TestTree-simpleMultiTest = testCase "simple-multi-test" $ withLongTimeout $ runWithExtraFiles "multi" $ \dir -> do- let aPath = dir </> "a/A.hs"- bPath = dir </> "b/B.hs"- adoc <- openDoc aPath "haskell"- bdoc <- openDoc bPath "haskell"- WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" adoc- liftIO $ assertBool "A should typecheck" ideResultSuccess- WaitForIdeRuleResult {..} <- waitForAction "TypeCheck" bdoc- liftIO $ assertBool "B should typecheck" ideResultSuccess- locs <- getDefinitions bdoc (Position 2 7)- let fooL = mkL (adoc ^. L.uri) 2 0 2 3- checkDefs locs (pure [fooL])- expectNoMoreDiagnostics 0.5---- Like simpleMultiTest but open the files in the other order-simpleMultiTest2 :: TestTree-simpleMultiTest2 = testCase "simple-multi-test2" $ runWithExtraFiles "multi" $ \dir -> do- let aPath = dir </> "a/A.hs"- bPath = dir </> "b/B.hs"- bdoc <- openDoc bPath "haskell"- WaitForIdeRuleResult {} <- waitForAction "TypeCheck" bdoc- TextDocumentIdentifier auri <- openDoc aPath "haskell"- skipManyTill anyMessage $ isReferenceReady aPath- locs <- getDefinitions bdoc (Position 2 7)- let fooL = mkL auri 2 0 2 3- checkDefs locs (pure [fooL])- expectNoMoreDiagnostics 0.5---- Now with 3 components-simpleMultiTest3 :: TestTree-simpleMultiTest3 =- testCase "simple-multi-test3" $ runWithExtraFiles "multi" $ \dir -> do- let aPath = dir </> "a/A.hs"- bPath = dir </> "b/B.hs"- cPath = dir </> "c/C.hs"- bdoc <- openDoc bPath "haskell"- WaitForIdeRuleResult {} <- waitForAction "TypeCheck" bdoc- TextDocumentIdentifier auri <- openDoc aPath "haskell"- skipManyTill anyMessage $ isReferenceReady aPath- cdoc <- openDoc cPath "haskell"- WaitForIdeRuleResult {} <- waitForAction "TypeCheck" cdoc- locs <- getDefinitions cdoc (Position 2 7)- let fooL = mkL auri 2 0 2 3- checkDefs locs (pure [fooL])- expectNoMoreDiagnostics 0.5---- Like simpleMultiTest but open the files in component 'a' in a separate session-simpleMultiDefTest :: TestTree-simpleMultiDefTest = testCase "simple-multi-def-test" $ runWithExtraFiles "multi" $ \dir -> do- let aPath = dir </> "a/A.hs"- bPath = dir </> "b/B.hs"- adoc <- liftIO $ runInDir dir $ do- aSource <- liftIO $ readFileUtf8 aPath- adoc <- createDoc aPath "haskell" aSource- skipManyTill anyMessage $ isReferenceReady aPath- closeDoc adoc- pure adoc- bSource <- liftIO $ readFileUtf8 bPath- bdoc <- createDoc bPath "haskell" bSource- locs <- getDefinitions bdoc (Position 2 7)- let fooL = mkL (adoc ^. L.uri) 2 0 2 3- checkDefs locs (pure [fooL])- expectNoMoreDiagnostics 0.5--ifaceTests :: TestTree-ifaceTests = testGroup "Interface loading tests"- [ -- https://github.com/haskell/ghcide/pull/645/- ifaceErrorTest- , ifaceErrorTest2- , ifaceErrorTest3- , ifaceTHTest- ]--bootTests :: TestTree-bootTests = testGroup "boot"- [ testCase "boot-def-test" $ runWithExtraFiles "boot" $ \dir -> do- let cPath = dir </> "C.hs"- cSource <- liftIO $ readFileUtf8 cPath- -- Dirty the cache- liftIO $ runInDir dir $ do- cDoc <- createDoc cPath "haskell" cSource- -- We send a hover request then wait for either the hover response or- -- `ghcide/reference/ready` notification.- -- Once we receive one of the above, we wait for the other that we- -- haven't received yet.- -- If we don't wait for the `ready` notification it is possible- -- that the `getDefinitions` request/response in the outer ghcide- -- session will find no definitions.- let hoverParams = HoverParams cDoc (Position 4 3) Nothing- hoverRequestId <- sendRequest STextDocumentHover hoverParams- let parseReadyMessage = isReferenceReady cPath- let parseHoverResponse = responseForId STextDocumentHover hoverRequestId- hoverResponseOrReadyMessage <- skipManyTill anyMessage ((Left <$> parseHoverResponse) <|> (Right <$> parseReadyMessage))- _ <- skipManyTill anyMessage $- case hoverResponseOrReadyMessage of- Left _ -> void parseReadyMessage- Right _ -> void parseHoverResponse- closeDoc cDoc- cdoc <- createDoc cPath "haskell" cSource- locs <- getDefinitions cdoc (Position 7 4)- let floc = mkR 9 0 9 1- checkDefs locs (pure [floc])- , testCase "graph with boot modules" $ runWithExtraFiles "boot2" $ \dir -> do- _ <- openDoc (dir </> "A.hs") "haskell"- expectNoMoreDiagnostics 2- ]---- | test that TH reevaluates across interfaces-ifaceTHTest :: TestTree-ifaceTHTest = testCase "iface-th-test" $ runWithExtraFiles "TH" $ \dir -> do- let aPath = dir </> "THA.hs"- bPath = dir </> "THB.hs"- cPath = dir </> "THC.hs"-- aSource <- liftIO $ readFileUtf8 aPath -- [TH] a :: ()- _bSource <- liftIO $ readFileUtf8 bPath -- a :: ()- cSource <- liftIO $ readFileUtf8 cPath -- c = a :: ()-- cdoc <- createDoc cPath "haskell" cSource-- -- Change [TH]a from () to Bool- liftIO $ writeFileUTF8 aPath (unlines $ init (lines $ T.unpack aSource) ++ ["th_a = [d| a = False|]"])-- -- Check that the change propagates to C- changeDoc cdoc [TextDocumentContentChangeEvent Nothing Nothing cSource]- expectDiagnostics- [("THC.hs", [(DsError, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'")])- ,("THB.hs", [(DsWarning, (4,thDollarIdx), "Top-level binding")])]- closeDoc cdoc--ifaceErrorTest :: TestTree-ifaceErrorTest = testCase "iface-error-test-1" $ runWithExtraFiles "recomp" $ \dir -> do- configureCheckProject True- let bPath = dir </> "B.hs"- pPath = dir </> "P.hs"-- bSource <- liftIO $ readFileUtf8 bPath -- y :: Int- pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int-- bdoc <- createDoc bPath "haskell" bSource- expectDiagnostics- [("P.hs", [(DsWarning,(4,0), "Top-level binding")])] -- So what we know P has been loaded-- -- Change y from Int to B- changeDoc bdoc [TextDocumentContentChangeEvent Nothing Nothing $ T.unlines ["module B where", "y :: Bool", "y = undefined"]]- -- save so that we can that the error propagates to A- sendNotification STextDocumentDidSave (DidSaveTextDocumentParams bdoc Nothing)--- -- Check that the error propagates to A- expectDiagnostics- [("A.hs", [(DsError, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])]-- -- Check that we wrote the interfaces for B when we saved- hidir <- getInterfaceFilesDir bdoc- hi_exists <- liftIO $ doesFileExist $ hidir </> "B.hi"- liftIO $ assertBool ("Couldn't find B.hi in " ++ hidir) hi_exists-- pdoc <- openDoc pPath "haskell"- expectDiagnostics- [("P.hs", [(DsWarning,(4,0), "Top-level binding")])- ]- changeDoc pdoc [TextDocumentContentChangeEvent Nothing Nothing $ pSource <> "\nfoo = y :: Bool" ]- -- Now in P we have- -- bar = x :: Int- -- foo = y :: Bool- -- HOWEVER, in A...- -- x = y :: Int- -- This is clearly inconsistent, and the expected outcome a bit surprising:- -- - The diagnostic for A has already been received. Ghcide does not repeat diagnostics- -- - P is being typechecked with the last successful artifacts for A.- expectDiagnostics- [("P.hs", [(DsWarning,(4,0), "Top-level binding")])- ,("P.hs", [(DsWarning,(6,0), "Top-level binding")])- ]- expectNoMoreDiagnostics 2--ifaceErrorTest2 :: TestTree-ifaceErrorTest2 = testCase "iface-error-test-2" $ runWithExtraFiles "recomp" $ \dir -> do- let bPath = dir </> "B.hs"- pPath = dir </> "P.hs"-- bSource <- liftIO $ readFileUtf8 bPath -- y :: Int- pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int-- bdoc <- createDoc bPath "haskell" bSource- pdoc <- createDoc pPath "haskell" pSource- expectDiagnostics- [("P.hs", [(DsWarning,(4,0), "Top-level binding")])] -- So that we know P has been loaded-- -- Change y from Int to B- changeDoc bdoc [TextDocumentContentChangeEvent Nothing Nothing $ T.unlines ["module B where", "y :: Bool", "y = undefined"]]-- -- Add a new definition to P- changeDoc pdoc [TextDocumentContentChangeEvent Nothing Nothing $ pSource <> "\nfoo = y :: Bool" ]- -- Now in P we have- -- bar = x :: Int- -- foo = y :: Bool- -- HOWEVER, in A...- -- x = y :: Int- expectDiagnostics- -- As in the other test, P is being typechecked with the last successful artifacts for A- -- (ot thanks to -fdeferred-type-errors)- [("A.hs", [(DsError, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])- ,("P.hs", [(DsWarning, (4, 0), "Top-level binding")])- ,("P.hs", [(DsWarning, (6, 0), "Top-level binding")])- ]-- expectNoMoreDiagnostics 2--ifaceErrorTest3 :: TestTree-ifaceErrorTest3 = testCase "iface-error-test-3" $ runWithExtraFiles "recomp" $ \dir -> do- let bPath = dir </> "B.hs"- pPath = dir </> "P.hs"-- bSource <- liftIO $ readFileUtf8 bPath -- y :: Int- pSource <- liftIO $ readFileUtf8 pPath -- bar = x :: Int-- bdoc <- createDoc bPath "haskell" bSource-- -- Change y from Int to B- changeDoc bdoc [TextDocumentContentChangeEvent Nothing Nothing $ T.unlines ["module B where", "y :: Bool", "y = undefined"]]-- -- P should not typecheck, as there are no last valid artifacts for A- _pdoc <- createDoc pPath "haskell" pSource-- -- In this example the interface file for A should not exist (modulo the cache folder)- -- Despite that P still type checks, as we can generate an interface file for A thanks to -fdeferred-type-errors- expectDiagnostics- [("A.hs", [(DsError, (5, 4), "Couldn't match expected type 'Int' with actual type 'Bool'")])- ,("P.hs", [(DsWarning,(4,0), "Top-level binding")])- ]- expectNoMoreDiagnostics 2--sessionDepsArePickedUp :: TestTree-sessionDepsArePickedUp = testSession'- "session-deps-are-picked-up"- $ \dir -> do- liftIO $- writeFileUTF8- (dir </> "hie.yaml")- "cradle: {direct: {arguments: []}}"- -- Open without OverloadedStrings and expect an error.- doc <- createDoc "Foo.hs" "haskell" fooContent- expectDiagnostics $- if ghcVersion >= GHC90- -- String vs [Char] causes this change in error message- then [("Foo.hs", [(DsError, (3, 6), "Couldn't match type")])]- else [("Foo.hs", [(DsError, (3, 6), "Couldn't match expected type")])]- -- Update hie.yaml to enable OverloadedStrings.- liftIO $- writeFileUTF8- (dir </> "hie.yaml")- "cradle: {direct: {arguments: [-XOverloadedStrings]}}"- sendNotification SWorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $- List [FileEvent (filePathToUri $ dir </> "hie.yaml") FcChanged ]- -- Send change event.- let change =- TextDocumentContentChangeEvent- { _range = Just (Range (Position 4 0) (Position 4 0)),- _rangeLength = Nothing,- _text = "\n"- }- changeDoc doc [change]- -- Now no errors.- expectDiagnostics [("Foo.hs", [])]- where- fooContent =- T.unlines- [ "module Foo where",- "import Data.Text",- "foo :: Text",- "foo = \"hello\""- ]---- A test to ensure that the command line ghcide workflow stays working-nonLspCommandLine :: TestTree-nonLspCommandLine = testGroup "ghcide command line"- [ testCase "works" $ withTempDir $ \dir -> do- ghcide <- locateGhcideExecutable- copyTestDataFiles dir "multi"- let cmd = (proc ghcide ["a/A.hs"]){cwd = Just dir}-- setEnv "HOME" "/homeless-shelter" False-- (ec, _, _) <- readCreateProcessWithExitCode cmd ""-- ec @?= ExitSuccess- ]---- | checks if we use InitializeParams.rootUri for loading session-rootUriTests :: TestTree-rootUriTests = testCase "use rootUri" . runTest "dirA" "dirB" $ \dir -> do- let bPath = dir </> "dirB/Foo.hs"- liftIO $ copyTestDataFiles dir "rootUri"- bSource <- liftIO $ readFileUtf8 bPath- _ <- createDoc "Foo.hs" "haskell" bSource- expectNoMoreDiagnostics 0.5- where- -- similar to run' except we can configure where to start ghcide and session- runTest :: FilePath -> FilePath -> (FilePath -> Session ()) -> IO ()- runTest dir1 dir2 s = withTempDir $ \dir -> runInDir' dir dir1 dir2 [] (s dir)---- | Test if ghcide asynchronously handles Commands and user Requests-asyncTests :: TestTree-asyncTests = testGroup "async"- [- testSession "command" $ do- -- Execute a command that will block forever- let req = ExecuteCommandParams Nothing blockCommandId Nothing- void $ sendRequest SWorkspaceExecuteCommand req- -- Load a file and check for code actions. Will only work if the command is run asynchronously- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS -Wmissing-signatures #-}"- , "foo = id"- ]- void waitForDiagnostics- codeLenses <- getCodeLenses doc- liftIO $ [ _title | CodeLens{_command = Just Command{_title}} <- codeLenses] @=?- [ "foo :: a -> a" ]- , testSession "request" $ do- -- Execute a custom request that will block for 1000 seconds- void $ sendRequest (SCustomMethod "test") $ toJSON $ BlockSeconds 1000- -- Load a file and check for code actions. Will only work if the request is run asynchronously- doc <- createDoc "A.hs" "haskell" $ T.unlines- [ "{-# OPTIONS -Wmissing-signatures #-}"- , "foo = id"- ]- void waitForDiagnostics- codeLenses <- getCodeLenses doc- liftIO $ [ _title | CodeLens{_command = Just Command{_title}} <- codeLenses] @=?- [ "foo :: a -> a" ]- ]---clientSettingsTest :: TestTree-clientSettingsTest = testGroup "client settings handling"- [ testSession "ghcide restarts shake session on config changes" $ do- void $ skipManyTill anyMessage $ message SClientRegisterCapability- void $ createDoc "A.hs" "haskell" "module A where"- waitForProgressDone- sendNotification SWorkspaceDidChangeConfiguration- (DidChangeConfigurationParams (toJSON (mempty :: A.Object)))- skipManyTill anyMessage restartingBuildSession-- ]- where- restartingBuildSession :: Session ()- restartingBuildSession = do- FromServerMess SWindowLogMessage NotificationMessage{_params = LogMessageParams{..}} <- loggingNotification- guard $ "Restarting build session" `T.isInfixOf` _message--referenceTests :: TestTree-referenceTests = testGroup "references"- [ testGroup "can get references to FOIs"- [ referenceTest "can get references to symbols"- ("References.hs", 4, 7)- YesIncludeDeclaration- [ ("References.hs", 4, 6)- , ("References.hs", 6, 0)- , ("References.hs", 6, 14)- , ("References.hs", 9, 7)- , ("References.hs", 10, 11)- ]-- , referenceTest "can get references to data constructor"- ("References.hs", 13, 2)- YesIncludeDeclaration- [ ("References.hs", 13, 2)- , ("References.hs", 16, 14)- , ("References.hs", 19, 21)- ]-- , referenceTest "getting references works in the other module"- ("OtherModule.hs", 6, 0)- YesIncludeDeclaration- [ ("OtherModule.hs", 6, 0)- , ("OtherModule.hs", 8, 16)- ]-- , referenceTest "getting references works in the Main module"- ("Main.hs", 9, 0)- YesIncludeDeclaration- [ ("Main.hs", 9, 0)- , ("Main.hs", 10, 4)- ]-- , referenceTest "getting references to main works"- ("Main.hs", 5, 0)- YesIncludeDeclaration- [ ("Main.hs", 4, 0)- , ("Main.hs", 5, 0)- ]-- , referenceTest "can get type references"- ("Main.hs", 9, 9)- YesIncludeDeclaration- [ ("Main.hs", 9, 0)- , ("Main.hs", 9, 9)- , ("Main.hs", 10, 0)- ]-- , expectFailBecause "references provider does not respect includeDeclaration parameter" $- referenceTest "works when we ask to exclude declarations"- ("References.hs", 4, 7)- NoExcludeDeclaration- [ ("References.hs", 6, 0)- , ("References.hs", 6, 14)- , ("References.hs", 9, 7)- , ("References.hs", 10, 11)- ]-- , referenceTest "INCORRECTLY returns declarations when we ask to exclude them"- ("References.hs", 4, 7)- NoExcludeDeclaration- [ ("References.hs", 4, 6)- , ("References.hs", 6, 0)- , ("References.hs", 6, 14)- , ("References.hs", 9, 7)- , ("References.hs", 10, 11)- ]- ]-- , testGroup "can get references to non FOIs"- [ referenceTest "can get references to symbol defined in a module we import"- ("References.hs", 22, 4)- YesIncludeDeclaration- [ ("References.hs", 22, 4)- , ("OtherModule.hs", 0, 20)- , ("OtherModule.hs", 4, 0)- ]-- , referenceTest "can get references in modules that import us to symbols we define"- ("OtherModule.hs", 4, 0)- YesIncludeDeclaration- [ ("References.hs", 22, 4)- , ("OtherModule.hs", 0, 20)- , ("OtherModule.hs", 4, 0)- ]-- , referenceTest "can get references to symbol defined in a module we import transitively"- ("References.hs", 24, 4)- YesIncludeDeclaration- [ ("References.hs", 24, 4)- , ("OtherModule.hs", 0, 48)- , ("OtherOtherModule.hs", 2, 0)- ]-- , referenceTest "can get references in modules that import us transitively to symbols we define"- ("OtherOtherModule.hs", 2, 0)- YesIncludeDeclaration- [ ("References.hs", 24, 4)- , ("OtherModule.hs", 0, 48)- , ("OtherOtherModule.hs", 2, 0)- ]-- , referenceTest "can get type references to other modules"- ("Main.hs", 12, 10)- YesIncludeDeclaration- [ ("Main.hs", 12, 7)- , ("Main.hs", 13, 0)- , ("References.hs", 12, 5)- , ("References.hs", 16, 0)- ]- ]- ]---- | When we ask for all references to symbol "foo", should the declaration "foo--- = 2" be among the references returned?-data IncludeDeclaration =- YesIncludeDeclaration- | NoExcludeDeclaration--getReferences' :: SymbolLocation -> IncludeDeclaration -> Session (List Location)-getReferences' (file, l, c) includeDeclaration = do- doc <- openDoc file "haskell"- getReferences doc (Position l c) $ toBool includeDeclaration- where toBool YesIncludeDeclaration = True- toBool NoExcludeDeclaration = False--referenceTestSession :: String -> FilePath -> [FilePath] -> (FilePath -> Session ()) -> TestTree-referenceTestSession name thisDoc docs' f = testSessionWithExtraFiles "references" name $ \dir -> do- -- needed to build whole project indexing- configureCheckProject True- let docs = map (dir </>) $ delete thisDoc $ nubOrd docs'- -- Initial Index- docid <- openDoc thisDoc "haskell"- let- loop :: [FilePath] -> Session ()- loop [] = pure ()- loop docs = do- doc <- skipManyTill anyMessage $ referenceReady (`elem` docs)- loop (delete doc docs)- loop docs- f dir- closeDoc docid---- | Given a location, lookup the symbol and all references to it. Make sure--- they are the ones we expect.-referenceTest :: String -> SymbolLocation -> IncludeDeclaration -> [SymbolLocation] -> TestTree-referenceTest name loc includeDeclaration expected =- referenceTestSession name (fst3 loc) docs $ \dir -> do- List actual <- getReferences' loc includeDeclaration- liftIO $ actual `expectSameLocations` map (first3 (dir </>)) expected- where- docs = map fst3 expected--type SymbolLocation = (FilePath, UInt, UInt)--expectSameLocations :: [Location] -> [SymbolLocation] -> Assertion-expectSameLocations actual expected = do- let actual' =- Set.map (\location -> (location ^. L.uri- , location ^. L.range . L.start . L.line . to fromIntegral- , location ^. L.range . L.start . L.character . to fromIntegral))- $ Set.fromList actual- expected' <- Set.fromList <$>- (forM expected $ \(file, l, c) -> do- fp <- canonicalizePath file- return (filePathToUri fp, l, c))- actual' @?= expected'--------------------------------------------------------------------------- Utils-------------------------------------------------------------------------testSession :: String -> Session () -> TestTree-testSession name = testCase name . run--testSessionWithExtraFiles :: FilePath -> String -> (FilePath -> Session ()) -> TestTree-testSessionWithExtraFiles prefix name = testCase name . runWithExtraFiles prefix--testSession' :: String -> (FilePath -> Session ()) -> TestTree-testSession' name = testCase name . run'--testSessionWait :: HasCallStack => String -> Session () -> TestTree-testSessionWait name = testSession name .- -- Check that any diagnostics produced were already consumed by the test case.- --- -- If in future we add test cases where we don't care about checking the diagnostics,- -- this could move elsewhere.- --- -- Experimentally, 0.5s seems to be long enough to wait for any final diagnostics to appear.- ( >> expectNoMoreDiagnostics 0.5)--mkRange :: UInt -> UInt -> UInt -> UInt -> Range-mkRange a b c d = Range (Position a b) (Position c d)--run :: Session a -> IO a-run s = run' (const s)--runWithExtraFiles :: FilePath -> (FilePath -> Session a) -> IO a-runWithExtraFiles prefix s = withTempDir $ \dir -> do- copyTestDataFiles dir prefix- runInDir dir (s dir)--copyTestDataFiles :: FilePath -> FilePath -> IO ()-copyTestDataFiles dir prefix = do- -- Copy all the test data files to the temporary workspace- testDataFiles <- getDirectoryFilesIO ("test/data" </> prefix) ["//*"]- for_ testDataFiles $ \f -> do- createDirectoryIfMissing True $ dir </> takeDirectory f- copyFile ("test/data" </> prefix </> f) (dir </> f)--run' :: (FilePath -> Session a) -> IO a-run' s = withTempDir $ \dir -> runInDir dir (s dir)--runInDir :: FilePath -> Session a -> IO a-runInDir dir = runInDir' dir "." "." []--withLongTimeout :: IO a -> IO a-withLongTimeout = bracket_ (setEnv "LSP_TIMEOUT" "120" True) (unsetEnv "LSP_TIMEOUT")---- | Takes a directory as well as relative paths to where we should launch the executable as well as the session root.-runInDir' :: FilePath -> FilePath -> FilePath -> [String] -> Session a -> IO a-runInDir' = runInDir'' lspTestCaps--runInDir''- :: ClientCapabilities- -> FilePath- -> FilePath- -> FilePath- -> [String]- -> Session b- -> IO b-runInDir'' lspCaps dir startExeIn startSessionIn extraOptions s = do-- ghcideExe <- locateGhcideExecutable- let startDir = dir </> startExeIn- let projDir = dir </> startSessionIn-- createDirectoryIfMissing True startDir- createDirectoryIfMissing True projDir- -- Temporarily hack around https://github.com/mpickering/hie-bios/pull/56- -- since the package import test creates "Data/List.hs", which otherwise has no physical home- createDirectoryIfMissing True $ projDir ++ "/Data"-- shakeProfiling <- getEnv "SHAKE_PROFILING"- let cmd = unwords $- [ghcideExe, "--lsp", "--test", "--verify-core-file", "--verbose", "-j2", "--cwd", startDir- ] ++ ["--shake-profiling=" <> dir | Just dir <- [shakeProfiling]- ] ++ extraOptions- -- HIE calls getXgdDirectory which assumes that HOME is set.- -- Only sets HOME if it wasn't already set.- setEnv "HOME" "/homeless-shelter" False- conf <- getConfigFromEnv- runSessionWithConfig conf cmd lspCaps projDir $ do- configureCheckProject False- s---getConfigFromEnv :: IO SessionConfig-getConfigFromEnv = do- logColor <- fromMaybe True <$> checkEnv "LSP_TEST_LOG_COLOR"- timeoutOverride <- fmap read <$> getEnv "LSP_TIMEOUT"- return defaultConfig- { messageTimeout = fromMaybe (messageTimeout defaultConfig) timeoutOverride- , logColor- }- where- checkEnv :: String -> IO (Maybe Bool)- checkEnv s = fmap convertVal <$> getEnv s- convertVal "0" = False- convertVal _ = True--lspTestCaps :: ClientCapabilities-lspTestCaps = fullCaps { _window = Just $ WindowClientCapabilities (Just True) Nothing Nothing }--lspTestCapsNoFileWatches :: ClientCapabilities-lspTestCapsNoFileWatches = lspTestCaps & workspace . Lens._Just . didChangeWatchedFiles .~ Nothing--openTestDataDoc :: FilePath -> Session TextDocumentIdentifier-openTestDataDoc path = do- source <- liftIO $ readFileUtf8 $ "test/data" </> path- createDoc path "haskell" source--unitTests :: Recorder (WithPriority Log) -> Logger -> TestTree-unitTests recorder logger = do- testGroup "Unit"- [ testCase "empty file path does NOT work with the empty String literal" $- uriToFilePath' (fromNormalizedUri $ filePathToUri' "") @?= Just "."- , testCase "empty file path works using toNormalizedFilePath'" $- uriToFilePath' (fromNormalizedUri $ filePathToUri' (toNormalizedFilePath' "")) @?= Just ""- , testCase "empty path URI" $ do- Just URI{..} <- pure $ parseURI (T.unpack $ getUri $ fromNormalizedUri emptyPathUri)- uriScheme @?= "file:"- uriPath @?= ""- , testCase "from empty path URI" $ do- let uri = Uri "file://"- uriToFilePath' uri @?= Just ""- , testCase "showDiagnostics prints ranges 1-based (like vscode)" $ do- let diag = ("", Diagnostics.ShowDiag, Diagnostic- { _range = Range- { _start = Position{_line = 0, _character = 1}- , _end = Position{_line = 2, _character = 3}- }- , _severity = Nothing- , _code = Nothing- , _source = Nothing- , _message = ""- , _relatedInformation = Nothing- , _tags = Nothing- })- let shown = T.unpack (Diagnostics.showDiagnostics [diag])- let expected = "1:2-3:4"- assertBool (unwords ["expected to find range", expected, "in diagnostic", shown]) $- expected `isInfixOf` shown- , testCase "notification handlers run in priority order" $ do- orderRef <- newIORef []- let plugins = pluginDescToIdePlugins $- [ (priorityPluginDescriptor i)- { pluginNotificationHandlers = mconcat- [ mkPluginNotificationHandler LSP.STextDocumentDidOpen $ \_ _ _ _ ->- liftIO $ atomicModifyIORef_ orderRef (i:)- ]- }- | i <- [1..20]- ] ++ Ghcide.descriptors (cmapWithPrio LogGhcIde recorder)- priorityPluginDescriptor i = (defaultPluginDescriptor $ fromString $ show i){pluginPriority = i}-- testIde recorder (IDE.testing (cmapWithPrio LogIDEMain recorder) logger plugins) $ do- _ <- createDoc "A.hs" "haskell" "module A where"- waitForProgressDone- actualOrder <- liftIO $ reverse <$> readIORef orderRef-- -- Handlers are run in priority descending order- liftIO $ actualOrder @?= [20, 19 .. 1]- , ignoreTestBecause "The test fails sometimes showing 10000us" $- testCase "timestamps have millisecond resolution" $ do- resolution_us <- findResolution_us 1- let msg = printf "Timestamps do not have millisecond resolution: %dus" resolution_us- assertBool msg (resolution_us <= 1000)- , Progress.tests- , FuzzySearch.tests- ]--garbageCollectionTests :: TestTree-garbageCollectionTests = testGroup "garbage collection"- [ testGroup "dirty keys"- [ testSession' "are collected" $ \dir -> do- liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A]}}"- doc <- generateGarbage "A" dir- closeDoc doc- garbage <- waitForGC- liftIO $ assertBool "no garbage was found" $ not $ null garbage-- , testSession' "are deleted from the state" $ \dir -> do- liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A]}}"- docA <- generateGarbage "A" dir- keys0 <- getStoredKeys- closeDoc docA- garbage <- waitForGC- liftIO $ assertBool "something is wrong with this test - no garbage found" $ not $ null garbage- keys1 <- getStoredKeys- liftIO $ assertBool "keys were not deleted from the state" (length keys1 < length keys0)-- , testSession' "are not regenerated unless needed" $ \dir -> do- liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A.hs, B.hs]}}"- docA <- generateGarbage "A" dir- _docB <- generateGarbage "B" dir-- -- garbage collect A keys- keysBeforeGC <- getStoredKeys- closeDoc docA- garbage <- waitForGC- liftIO $ assertBool "something is wrong with this test - no garbage found" $ not $ null garbage- keysAfterGC <- getStoredKeys- liftIO $ assertBool "something is wrong with this test - keys were not deleted from the state"- (length keysAfterGC < length keysBeforeGC)-- -- re-typecheck B and check that the keys for A have not materialized back- _docB <- generateGarbage "B" dir- keysB <- getStoredKeys- let regeneratedKeys = Set.filter (not . isExpected) $- Set.intersection (Set.fromList garbage) (Set.fromList keysB)- liftIO $ regeneratedKeys @?= mempty-- , testSession' "regenerate successfully" $ \dir -> do- liftIO $ writeFile (dir </> "hie.yaml") "cradle: {direct: {arguments: [A]}}"- docA <- generateGarbage "A" dir- closeDoc docA- garbage <- waitForGC- liftIO $ assertBool "no garbage was found" $ not $ null garbage- let edit = T.unlines- [ "module A where"- , "a :: Bool"- , "a = ()"- ]- doc <- generateGarbage "A" dir- changeDoc doc [TextDocumentContentChangeEvent Nothing Nothing edit]- builds <- waitForTypecheck doc- liftIO $ assertBool "it still builds" builds- expectCurrentDiagnostics doc [(DsError, (2,4), "Couldn't match expected type")]- ]- ]- where- isExpected k = any (`T.isPrefixOf` k) ["GhcSessionIO"]-- generateGarbage :: String -> FilePath -> Session TextDocumentIdentifier- generateGarbage modName dir = do- let fp = modName <> ".hs"- body = printf "module %s where" modName- doc <- createDoc fp "haskell" (T.pack body)- liftIO $ writeFile (dir </> fp) body- builds <- waitForTypecheck doc- liftIO $ assertBool "something is wrong with this test" builds- return doc--findResolution_us :: Int -> IO Int-findResolution_us delay_us | delay_us >= 1000000 = error "Unable to compute timestamp resolution"-findResolution_us delay_us = withTempFile $ \f -> withTempFile $ \f' -> do- performGC- writeFile f ""- threadDelay delay_us- writeFile f' ""- t <- getModTime f- t' <- getModTime f'- if t /= t' then return delay_us else findResolution_us (delay_us * 10)---testIde :: Recorder (WithPriority Log) -> IDE.Arguments -> Session () -> IO ()-testIde recorder arguments session = do- config <- getConfigFromEnv- cwd <- getCurrentDirectory- (hInRead, hInWrite) <- createPipe- (hOutRead, hOutWrite) <- createPipe- let projDir = "."- let server = IDE.defaultMain (cmapWithPrio LogIDEMain recorder) arguments- { IDE.argsHandleIn = pure hInRead- , IDE.argsHandleOut = pure hOutWrite- }-- flip finally (setCurrentDirectory cwd) $ withAsync server $ \_ ->- runSessionWithHandles hInWrite hOutRead config lspTestCaps projDir session--positionMappingTests :: Recorder (WithPriority Log) -> TestTree-positionMappingTests recorder =- testGroup "position mapping"- [ testGroup "toCurrent"- [ testCase "before" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "ab"- (Position 0 0) @?= PositionExact (Position 0 0)- , testCase "after, same line, same length" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "ab"- (Position 0 3) @?= PositionExact (Position 0 3)- , testCase "after, same line, increased length" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "abc"- (Position 0 3) @?= PositionExact (Position 0 4)- , testCase "after, same line, decreased length" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "a"- (Position 0 3) @?= PositionExact (Position 0 2)- , testCase "after, next line, no newline" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "abc"- (Position 1 3) @?= PositionExact (Position 1 3)- , testCase "after, next line, newline" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "abc\ndef"- (Position 1 0) @?= PositionExact (Position 2 0)- , testCase "after, same line, newline" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "abc\nd"- (Position 0 4) @?= PositionExact (Position 1 2)- , testCase "after, same line, newline + newline at end" $- toCurrent- (Range (Position 0 1) (Position 0 3))- "abc\nd\n"- (Position 0 4) @?= PositionExact (Position 2 1)- , testCase "after, same line, newline + newline at end" $- toCurrent- (Range (Position 0 1) (Position 0 1))- "abc"- (Position 0 1) @?= PositionExact (Position 0 4)- ]- , testGroup "fromCurrent"- [ testCase "before" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "ab"- (Position 0 0) @?= PositionExact (Position 0 0)- , testCase "after, same line, same length" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "ab"- (Position 0 3) @?= PositionExact (Position 0 3)- , testCase "after, same line, increased length" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "abc"- (Position 0 4) @?= PositionExact (Position 0 3)- , testCase "after, same line, decreased length" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "a"- (Position 0 2) @?= PositionExact (Position 0 3)- , testCase "after, next line, no newline" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "abc"- (Position 1 3) @?= PositionExact (Position 1 3)- , testCase "after, next line, newline" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "abc\ndef"- (Position 2 0) @?= PositionExact (Position 1 0)- , testCase "after, same line, newline" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "abc\nd"- (Position 1 2) @?= PositionExact (Position 0 4)- , testCase "after, same line, newline + newline at end" $- fromCurrent- (Range (Position 0 1) (Position 0 3))- "abc\nd\n"- (Position 2 1) @?= PositionExact (Position 0 4)- , testCase "after, same line, newline + newline at end" $- fromCurrent- (Range (Position 0 1) (Position 0 1))- "abc"- (Position 0 4) @?= PositionExact (Position 0 1)- ]- , adjustOption (\(QuickCheckTests i) -> QuickCheckTests (max 1000 i)) $ testGroup "properties"- [ testProperty "fromCurrent r t <=< toCurrent r t" $ do- -- Note that it is important to use suchThatMap on all values at once- -- instead of only using it on the position. Otherwise you can get- -- into situations where there is no position that can be mapped back- -- for the edit which will result in QuickCheck looping forever.- let gen = do- rope <- genRope- range <- genRange rope- PrintableText replacement <- arbitrary- oldPos <- genPosition rope- pure (range, replacement, oldPos)- forAll- (suchThatMap gen- (\(range, replacement, oldPos) -> positionResultToMaybe $ (range, replacement, oldPos,) <$> toCurrent range replacement oldPos)) $- \(range, replacement, oldPos, newPos) ->- fromCurrent range replacement newPos === PositionExact oldPos- , testProperty "toCurrent r t <=< fromCurrent r t" $ do- let gen = do- rope <- genRope- range <- genRange rope- PrintableText replacement <- arbitrary- let newRope = runIdentity $ applyChange mempty rope- (TextDocumentContentChangeEvent (Just range) Nothing replacement)- newPos <- genPosition newRope- pure (range, replacement, newPos)- forAll- (suchThatMap gen- (\(range, replacement, newPos) -> positionResultToMaybe $ (range, replacement, newPos,) <$> fromCurrent range replacement newPos)) $- \(range, replacement, newPos, oldPos) ->- toCurrent range replacement oldPos === PositionExact newPos- ]- ]--newtype PrintableText = PrintableText { getPrintableText :: T.Text }- deriving Show--instance Arbitrary PrintableText where- arbitrary = PrintableText . T.pack . getPrintableString <$> arbitrary---genRope :: Gen Rope-genRope = Rope.fromText . getPrintableText <$> arbitrary--genPosition :: Rope -> Gen Position-genPosition r = do- let rows :: Int = fromIntegral $ Rope.lengthInLines r- row <- choose (0, max 0 $ rows - 1) `suchThat` inBounds @UInt- let columns = T.length (nthLine (fromIntegral row) r)- column <- choose (0, max 0 $ columns - 1) `suchThat` inBounds @UInt- pure $ Position (fromIntegral row) (fromIntegral column)--genRange :: Rope -> Gen Range-genRange r = do- let rows :: Int = fromIntegral $ Rope.lengthInLines r- startPos@(Position startLine startColumn) <- genPosition r- let maxLineDiff = max 0 $ rows - 1 - fromIntegral startLine- endLine <- choose (fromIntegral startLine, fromIntegral startLine + maxLineDiff) `suchThat` inBounds @UInt- let columns = T.length (nthLine (fromIntegral endLine) r)- endColumn <-- if fromIntegral startLine == endLine- then choose (fromIntegral startColumn, columns)- else choose (0, max 0 $ columns - 1)- `suchThat` inBounds @UInt- pure $ Range startPos (Position (fromIntegral endLine) (fromIntegral endColumn))--inBounds :: forall b a . (Integral a, Integral b, Bounded b) => a -> Bool-inBounds a = let i = toInteger a in i <= toInteger (maxBound @b) && i >= toInteger (minBound @b)---- | Get the ith line of a rope, starting from 0. Trailing newline not included.-nthLine :: Int -> Rope -> T.Text-nthLine i r- | Rope.null r = ""- | otherwise = Rope.lines r !! i--getWatchedFilesSubscriptionsUntil :: forall m. SServerMethod m -> Session [DidChangeWatchedFilesRegistrationOptions]-getWatchedFilesSubscriptionsUntil m = do- msgs <- manyTill (Just <$> message SClientRegisterCapability <|> Nothing <$ anyMessage) (message m)- return- [ args- | Just RequestMessage{_params = RegistrationParams (List regs)} <- msgs- , SomeRegistration (Registration _id SWorkspaceDidChangeWatchedFiles args) <- regs- ]---- | Version of 'System.IO.Extra.withTempDir' that canonicalizes the path--- Which we need to do on macOS since the $TMPDIR can be in @/private/var@ or--- @/var@-withTempDir :: (FilePath -> IO a) -> IO a-withTempDir f = System.IO.Extra.withTempDir $ \dir -> do- dir' <- canonicalizePath dir- f dir'----- | Before ghc9, lists of Char is displayed as [Char], but with ghc9 and up, it's displayed as String-listOfChar :: T.Text-listOfChar | ghcVersion >= GHC90 = "String"- | otherwise = "[Char]"---- | Ghc 9 doesn't include the $-sign in TH warnings like earlier versions did-thDollarIdx :: UInt-thDollarIdx | ghcVersion >= GHC90 = 1- | otherwise = 0+++module Main (main) where+-- import Test.QuickCheck.Instances ()+import Data.Function ((&))+import Ide.Logger (Logger (Logger),+ LoggingColumn (DataColumn, PriorityColumn),+ Pretty (pretty),+ Priority (Debug),+ Recorder (Recorder, logger_),+ WithPriority (WithPriority, priority),+ cfilter,+ cmapWithPrio,+ makeDefaultStderrRecorder)+import GHC.Stack (emptyCallStack)+import qualified HieDbRetry+import Test.Tasty+import Test.Tasty.Ingredients.Rerun++import LogType ()+import OpenCloseTest+import InitializeResponseTests+import CompletionTests+import CPPTests+import DiagnosticTests+import CodeLensTests+import OutlineTests+import HighlightTests+import FindDefinitionAndHoverTests+import PluginSimpleTests+import PluginParsedResultTests+import PreprocessorTests+import THTests+import SymlinkTests+import SafeTests+import UnitTests+import HaddockTests+import PositionMappingTests+import WatchedFileTests+import CradleTests+import DependentFileTest+import NonLspCommandLine+import IfaceTests+import BootTests+import RootUriTests+import AsyncTests+import ClientSettingsTests+import ReferenceTests+import GarbageCollectionTests+import ExceptionTests++main :: IO ()+main = do+ docWithPriorityRecorder <- makeDefaultStderrRecorder (Just [PriorityColumn, DataColumn])++ let docWithFilteredPriorityRecorder@Recorder{ logger_ } =+ docWithPriorityRecorder+ & cfilter (\WithPriority{ priority } -> priority >= Debug)++ -- exists so old-style logging works. intended to be phased out+ let logger = Logger $ \p m -> logger_ (WithPriority p emptyCallStack (pretty m))++ let recorder = docWithFilteredPriorityRecorder+ & cmapWithPrio pretty++ -- We mess with env vars so run single-threaded.+ defaultMainWithRerun $ testGroup "ghcide"+ [ OpenCloseTest.tests+ , InitializeResponseTests.tests+ , CompletionTests.tests+ , CPPTests.tests+ , DiagnosticTests.tests+ , CodeLensTests.tests+ , OutlineTests.tests+ , HighlightTests.tests+ , FindDefinitionAndHoverTests.tests+ , PluginSimpleTests.tests+ , PluginParsedResultTests.tests+ , PreprocessorTests.tests+ , THTests.tests+ , SymlinkTests.tests+ , SafeTests.tests+ , UnitTests.tests recorder logger+ , HaddockTests.tests+ , PositionMappingTests.tests+ , WatchedFileTests.tests+ , CradleTests.tests+ , DependentFileTest.tests+ , NonLspCommandLine.tests+ , IfaceTests.tests+ , BootTests.tests+ , RootUriTests.tests+ , AsyncTests.tests+ , ClientSettingsTests.tests+ , ReferenceTests.tests+ , GarbageCollectionTests.tests+ , HieDbRetry.tests+ , ExceptionTests.tests recorder logger+ ]
+ test/exe/NonLspCommandLine.hs view
@@ -0,0 +1,27 @@++module NonLspCommandLine (tests) where++import Development.IDE.Test.Runfiles+import System.Environment.Blank (setEnv)+import System.Exit (ExitCode (ExitSuccess))+import System.Process.Extra (CreateProcess (cwd), proc,+ readCreateProcessWithExitCode)+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+++-- A test to ensure that the command line ghcide workflow stays working+tests :: TestTree+tests = testGroup "ghcide command line"+ [ testCase "works" $ withTempDir $ \dir -> do+ ghcide <- locateGhcideExecutable+ copyTestDataFiles dir "multi"+ let cmd = (proc ghcide ["a/A.hs"]){cwd = Just dir}++ setEnv "HOME" "/homeless-shelter" False++ (ec, _, _) <- readCreateProcessWithExitCode cmd ""++ ec @?= ExitSuccess+ ]
+ test/exe/OpenCloseTest.hs view
@@ -0,0 +1,18 @@++module OpenCloseTest (tests) where++import Control.Applicative.Combinators+import Control.Monad+import Language.LSP.Protocol.Message+import Language.LSP.Test+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import TestUtils++tests :: TestTree+tests = testSession "open close" $ do+ doc <- createDoc "Testing.hs" "haskell" ""+ void (skipManyTill anyMessage $ message SMethod_WindowWorkDoneProgressCreate)+ waitForProgressBegin+ closeDoc doc+ waitForProgressDone
+ test/exe/OutlineTests.hs view
@@ -0,0 +1,189 @@++module OutlineTests (tests) where++import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = testGroup+ "outline"+ [ testSessionWait "type class" $ do+ let source = T.unlines ["module A where", "class A a where a :: a -> Bool"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [ moduleSymbol+ "A"+ (R 0 7 0 8)+ [ classSymbol "A a"+ (R 1 0 1 30)+ [docSymbol' "a" SymbolKind_Method (R 1 16 1 30) (R 1 16 1 17)]+ ]+ ]+ , testSessionWait "type class instance " $ do+ let source = T.unlines ["class A a where", "instance A () where"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [ classSymbol "A a" (R 0 0 0 15) []+ , docSymbol "A ()" SymbolKind_Interface (R 1 0 1 19)+ ]+ , testSessionWait "type family" $ do+ let source = T.unlines ["{-# language TypeFamilies #-}", "type family A"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right [docSymbolD "A" "type family" SymbolKind_Function (R 1 0 1 13)]+ , testSessionWait "type family instance " $ do+ let source = T.unlines+ [ "{-# language TypeFamilies #-}"+ , "type family A a"+ , "type instance A () = ()"+ ]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [ docSymbolD "A a" "type family" SymbolKind_Function (R 1 0 1 15)+ , docSymbol "A ()" SymbolKind_Interface (R 2 0 2 23)+ ]+ , testSessionWait "data family" $ do+ let source = T.unlines ["{-# language TypeFamilies #-}", "data family A"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right [docSymbolD "A" "data family" SymbolKind_Function (R 1 0 1 11)]+ , testSessionWait "data family instance " $ do+ let source = T.unlines+ [ "{-# language TypeFamilies #-}"+ , "data family A a"+ , "data instance A () = A ()"+ ]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [ docSymbolD "A a" "data family" SymbolKind_Function (R 1 0 1 11)+ , docSymbol "A ()" SymbolKind_Interface (R 2 0 2 25)+ ]+ , testSessionWait "constant" $ do+ let source = T.unlines ["a = ()"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [docSymbol "a" SymbolKind_Function (R 0 0 0 6)]+ , testSessionWait "pattern" $ do+ let source = T.unlines ["Just foo = Just 21"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [docSymbol "Just foo" SymbolKind_Function (R 0 0 0 18)]+ , testSessionWait "pattern with type signature" $ do+ let source = T.unlines ["{-# language ScopedTypeVariables #-}", "a :: () = ()"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [docSymbol "a :: ()" SymbolKind_Function (R 1 0 1 12)]+ , testSessionWait "function" $ do+ let source = T.unlines ["a _x = ()"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right [docSymbol "a" SymbolKind_Function (R 0 0 0 9)]+ , testSessionWait "type synonym" $ do+ let source = T.unlines ["type A = Bool"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [docSymbol' "A" SymbolKind_TypeParameter (R 0 0 0 13) (R 0 5 0 6)]+ , testSessionWait "datatype" $ do+ let source = T.unlines ["data A = C"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [ docSymbolWithChildren "A"+ SymbolKind_Struct+ (R 0 0 0 10)+ [docSymbol "C" SymbolKind_Constructor (R 0 9 0 10)]+ ]+ , testSessionWait "record fields" $ do+ let source = T.unlines ["data A = B {", " x :: Int", " , y :: Int}"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [ docSymbolWithChildren "A" SymbolKind_Struct (R 0 0 2 13)+ [ docSymbolWithChildren' "B" SymbolKind_Constructor (R 0 9 2 13) (R 0 9 0 10)+ [ docSymbol "x" SymbolKind_Field (R 1 2 1 3)+ , docSymbol "y" SymbolKind_Field (R 2 4 2 5)+ ]+ ]+ ]+ , testSessionWait "import" $ do+ let source = T.unlines ["import Data.Maybe ()"]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [docSymbolWithChildren "imports"+ SymbolKind_Module+ (R 0 0 0 20)+ [ docSymbol "import Data.Maybe" SymbolKind_Module (R 0 0 0 20)+ ]+ ]+ , testSessionWait "multiple import" $ do+ let source = T.unlines ["", "import Data.Maybe ()", "", "import Control.Exception ()", ""]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right+ [docSymbolWithChildren "imports"+ SymbolKind_Module+ (R 1 0 3 27)+ [ docSymbol "import Data.Maybe" SymbolKind_Module (R 1 0 1 20)+ , docSymbol "import Control.Exception" SymbolKind_Module (R 3 0 3 27)+ ]+ ]+ , testSessionWait "foreign import" $ do+ let source = T.unlines+ [ "{-# language ForeignFunctionInterface #-}"+ , "foreign import ccall \"a\" a :: Int"+ ]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right [docSymbolD "a" "import" SymbolKind_Object (R 1 0 1 33)]+ , testSessionWait "foreign export" $ do+ let source = T.unlines+ [ "{-# language ForeignFunctionInterface #-}"+ , "foreign export ccall odd :: Int -> Bool"+ ]+ docId <- createDoc "A.hs" "haskell" source+ symbols <- getDocumentSymbols docId+ liftIO $ symbols @?= Right [docSymbolD "odd" "export" SymbolKind_Object (R 1 0 1 39)]+ ]+ where+ docSymbol name kind loc =+ DocumentSymbol name Nothing kind Nothing Nothing loc loc Nothing+ docSymbol' name kind loc selectionLoc =+ DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc Nothing+ docSymbolD name detail kind loc =+ DocumentSymbol name (Just detail) kind Nothing Nothing loc loc Nothing+ docSymbolWithChildren name kind loc cc =+ DocumentSymbol name Nothing kind Nothing Nothing loc loc (Just cc)+ docSymbolWithChildren' name kind loc selectionLoc cc =+ DocumentSymbol name Nothing kind Nothing Nothing loc selectionLoc (Just cc)+ moduleSymbol name loc cc = DocumentSymbol name+ Nothing+ SymbolKind_File+ Nothing+ Nothing+ (R 0 0 maxBound 0)+ loc+ (Just cc)+ classSymbol name loc cc = DocumentSymbol name+ (Just "class")+ SymbolKind_Interface+ Nothing+ Nothing+ loc+ loc+ (Just cc)
+ test/exe/PluginParsedResultTests.hs view
@@ -0,0 +1,17 @@++module PluginParsedResultTests (tests) where++import Development.IDE.Test (expectNoMoreDiagnostics)+import Language.LSP.Test+import System.FilePath+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import TestUtils++tests :: TestTree+tests =+ ignoreInWindowsForGHC810 $+ ignoreForGHC92Plus "No need for this plugin anymore!" $+ testSessionWithExtraFiles "plugin-recorddot" "parsedResultAction plugin" $ \dir -> do+ _ <- openDoc (dir</> "RecordDot.hs") "haskell"+ expectNoMoreDiagnostics 2
+ test/exe/PluginSimpleTests.hs view
@@ -0,0 +1,51 @@++module PluginSimpleTests (tests) where++import Control.Monad.IO.Class (liftIO)+import Development.IDE.GHC.Compat (GhcVersion (..))+import Development.IDE.Test (expectDiagnostics)+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+import System.FilePath+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import TestUtils++tests :: TestTree+tests =+ ignoreInWindowsForGHC810 $+ -- Build profile: -w ghc-9.4.2 -O1+ -- In order, the following will be built (use -v for more details):+ -- - ghc-typelits-natnormalise-0.7.7 (lib) (requires build)+ -- - ghc-typelits-knownnat-0.7.7 (lib) (requires build)+ -- - plugin-1.0.0 (lib) (first run)+ -- Starting ghc-typelits-natnormalise-0.7.7 (lib)+ -- Building ghc-typelits-natnormalise-0.7.7 (lib)++ -- Failed to build ghc-typelits-natnormalise-0.7.7.+ -- Build log (+ -- C:\cabal\logs\ghc-9.4.2\ghc-typelits-_-0.7.7-3f036a52a0d9bfc3389d1852a87da2e87c6de2e4.log+ -- ):+ -- Preprocessing library for ghc-typelits-natnormalise-0.7.7..+ -- Building library for ghc-typelits-natnormalise-0.7.7..+ -- [1 of 3] Compiling GHC.TypeLits.Normalise.SOP ( src\GHC\TypeLits\Normalise\SOP.hs, dist\build\GHC\TypeLits\Normalise\SOP.o )+ -- [2 of 3] Compiling GHC.TypeLits.Normalise.Unify ( src\GHC\TypeLits\Normalise\Unify.hs, dist\build\GHC\TypeLits\Normalise\Unify.o )+ -- [3 of 3] Compiling GHC.TypeLits.Normalise ( src-ghc-9.4\GHC\TypeLits\Normalise.hs, dist\build\GHC\TypeLits\Normalise.o )+ -- C:\tools\ghc-9.4.2\lib\../mingw/bin/llvm-ar.exe: error: dist\build\objs-5156\libHSghc-typelits-_-0.7.7-3f036a52a0d9bfc3389d1852a87da2e87c6de2e4.a: No such file or directory++ -- Error: cabal: Failed to build ghc-typelits-natnormalise-0.7.7 (which is+ -- required by plugin-1.0.0). See the build log above for details.+ ignoreFor (BrokenForGHC [GHC96]) "fragile, frequently times out" $+ ignoreFor (BrokenSpecific Windows [GHC94]) "ghc-typelist-natnormalise fails to build on GHC 9.4.2 for windows only" $+ testSessionWithExtraFiles "plugin-knownnat" "simple plugin" $ \dir -> do+ _ <- openDoc (dir </> "KnownNat.hs") "haskell"+ liftIO $ writeFile (dir</>"hie.yaml")+ "cradle: {cabal: [{path: '.', component: 'lib:plugin'}]}"++ expectDiagnostics+ [ ( "KnownNat.hs",+ [(DiagnosticSeverity_Error, (9, 15), "Variable not in scope: c")]+ )+ ]
+ test/exe/PositionMappingTests.hs view
@@ -0,0 +1,199 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE OverloadedLabels #-}++module PositionMappingTests (tests) where++import Data.Row+import qualified Data.Text as T+import Data.Text.Utf16.Rope (Rope)+import qualified Data.Text.Utf16.Rope as Rope+import Development.IDE.Core.PositionMapping (PositionResult (..),+ fromCurrent,+ positionResultToMaybe,+ toCurrent)+import Development.IDE.Types.Location+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.VFS (applyChange)+import Test.QuickCheck+-- import Test.QuickCheck.Instances ()+import Data.Functor.Identity (runIdentity)+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++tests :: TestTree+tests =+ testGroup "position mapping"+ [ testGroup "toCurrent"+ [ testCase "before" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "ab"+ (Position 0 0) @?= PositionExact (Position 0 0)+ , testCase "after, same line, same length" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "ab"+ (Position 0 3) @?= PositionExact (Position 0 3)+ , testCase "after, same line, increased length" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc"+ (Position 0 3) @?= PositionExact (Position 0 4)+ , testCase "after, same line, decreased length" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "a"+ (Position 0 3) @?= PositionExact (Position 0 2)+ , testCase "after, next line, no newline" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc"+ (Position 1 3) @?= PositionExact (Position 1 3)+ , testCase "after, next line, newline" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc\ndef"+ (Position 1 0) @?= PositionExact (Position 2 0)+ , testCase "after, same line, newline" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc\nd"+ (Position 0 4) @?= PositionExact (Position 1 2)+ , testCase "after, same line, newline + newline at end" $+ toCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc\nd\n"+ (Position 0 4) @?= PositionExact (Position 2 1)+ , testCase "after, same line, newline + newline at end" $+ toCurrent+ (Range (Position 0 1) (Position 0 1))+ "abc"+ (Position 0 1) @?= PositionExact (Position 0 4)+ ]+ , testGroup "fromCurrent"+ [ testCase "before" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "ab"+ (Position 0 0) @?= PositionExact (Position 0 0)+ , testCase "after, same line, same length" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "ab"+ (Position 0 3) @?= PositionExact (Position 0 3)+ , testCase "after, same line, increased length" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc"+ (Position 0 4) @?= PositionExact (Position 0 3)+ , testCase "after, same line, decreased length" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "a"+ (Position 0 2) @?= PositionExact (Position 0 3)+ , testCase "after, next line, no newline" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc"+ (Position 1 3) @?= PositionExact (Position 1 3)+ , testCase "after, next line, newline" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc\ndef"+ (Position 2 0) @?= PositionExact (Position 1 0)+ , testCase "after, same line, newline" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc\nd"+ (Position 1 2) @?= PositionExact (Position 0 4)+ , testCase "after, same line, newline + newline at end" $+ fromCurrent+ (Range (Position 0 1) (Position 0 3))+ "abc\nd\n"+ (Position 2 1) @?= PositionExact (Position 0 4)+ , testCase "after, same line, newline + newline at end" $+ fromCurrent+ (Range (Position 0 1) (Position 0 1))+ "abc"+ (Position 0 4) @?= PositionExact (Position 0 1)+ ]+ , adjustOption (\(QuickCheckTests i) -> QuickCheckTests (max 1000 i)) $ testGroup "properties"+ [ testProperty "fromCurrent r t <=< toCurrent r t" $ do+ -- Note that it is important to use suchThatMap on all values at once+ -- instead of only using it on the position. Otherwise you can get+ -- into situations where there is no position that can be mapped back+ -- for the edit which will result in QuickCheck looping forever.+ let gen = do+ rope <- genRope+ range <- genRange rope+ PrintableText replacement <- arbitrary+ oldPos <- genPosition rope+ pure (range, replacement, oldPos)+ forAll+ (suchThatMap gen+ (\(range, replacement, oldPos) -> positionResultToMaybe $ (range, replacement, oldPos,) <$> toCurrent range replacement oldPos)) $+ \(range, replacement, oldPos, newPos) ->+ fromCurrent range replacement newPos === PositionExact oldPos+ , testProperty "toCurrent r t <=< fromCurrent r t" $ do+ let gen = do+ rope <- genRope+ range <- genRange rope+ PrintableText replacement <- arbitrary+ let newRope = runIdentity $ applyChange mempty rope+ (TextDocumentContentChangeEvent $ InL $ #range .== range+ .+ #rangeLength .== Nothing+ .+ #text .== replacement)+ newPos <- genPosition newRope+ pure (range, replacement, newPos)+ forAll+ (suchThatMap gen+ (\(range, replacement, newPos) -> positionResultToMaybe $ (range, replacement, newPos,) <$> fromCurrent range replacement newPos)) $+ \(range, replacement, newPos, oldPos) ->+ toCurrent range replacement oldPos === PositionExact newPos+ ]+ ]++newtype PrintableText = PrintableText { getPrintableText :: T.Text }+ deriving Show++instance Arbitrary PrintableText where+ arbitrary = PrintableText . T.pack . getPrintableString <$> arbitrary++genRope :: Gen Rope+genRope = Rope.fromText . getPrintableText <$> arbitrary++genPosition :: Rope -> Gen Position+genPosition r = do+ let rows :: Int = fromIntegral $ Rope.lengthInLines r+ row <- choose (0, max 0 $ rows - 1) `suchThat` inBounds @UInt+ let columns = T.length (nthLine (fromIntegral row) r)+ column <- choose (0, max 0 $ columns - 1) `suchThat` inBounds @UInt+ pure $ Position (fromIntegral row) (fromIntegral column)++genRange :: Rope -> Gen Range+genRange r = do+ let rows :: Int = fromIntegral $ Rope.lengthInLines r+ startPos@(Position startLine startColumn) <- genPosition r+ let maxLineDiff = max 0 $ rows - 1 - fromIntegral startLine+ endLine <- choose (fromIntegral startLine, fromIntegral startLine + maxLineDiff) `suchThat` inBounds @UInt+ let columns = T.length (nthLine (fromIntegral endLine) r)+ endColumn <-+ if fromIntegral startLine == endLine+ then choose (fromIntegral startColumn, columns)+ else choose (0, max 0 $ columns - 1)+ `suchThat` inBounds @UInt+ pure $ Range startPos (Position (fromIntegral endLine) (fromIntegral endColumn))++inBounds :: forall b a . (Integral a, Integral b, Bounded b) => a -> Bool+inBounds a = let i = toInteger a in i <= toInteger (maxBound @b) && i >= toInteger (minBound @b)++-- | Get the ith line of a rope, starting from 0. Trailing newline not included.+nthLine :: Int -> Rope -> T.Text+nthLine i r+ | Rope.null r = ""+ | otherwise = Rope.lines r !! i
+ test/exe/PreprocessorTests.hs view
@@ -0,0 +1,27 @@++module PreprocessorTests (tests) where++import qualified Data.Text as T+import Development.IDE.Test (expectDiagnostics)+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import TestUtils++tests :: TestTree+tests = testSessionWait "preprocessor" $ do+ let content =+ T.unlines+ [ "{-# OPTIONS_GHC -F -pgmF=ghcide-test-preprocessor #-}"+ , "module Testing where"+ , "y = x + z" -- plugin replaces x with y, making this have only one diagnostic+ ]+ _ <- createDoc "Testing.hs" "haskell" content+ expectDiagnostics+ [ ( "Testing.hs",+ [(DiagnosticSeverity_Error, (2, 8), "Variable not in scope: z")]+ )+ ]
+ test/exe/ReferenceTests.hs view
@@ -0,0 +1,199 @@++module ReferenceTests (tests) where++import Control.Applicative.Combinators+import qualified Control.Lens as Lens+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.List.Extra+import qualified Data.Set as Set+import Development.IDE.Test (configureCheckProject,+ referenceReady)+import Development.IDE.Types.Location+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.Directory+import System.FilePath+-- import Test.QuickCheck.Instances ()+import Control.Lens ((^.))+import Data.Tuple.Extra+import Test.Tasty+import Test.Tasty.ExpectedFailure+import Test.Tasty.HUnit+import TestUtils+++tests :: TestTree+tests = testGroup "references"+ [ testGroup "can get references to FOIs"+ [ referenceTest "can get references to symbols"+ ("References.hs", 4, 7)+ YesIncludeDeclaration+ [ ("References.hs", 4, 6)+ , ("References.hs", 6, 0)+ , ("References.hs", 6, 14)+ , ("References.hs", 9, 7)+ , ("References.hs", 10, 11)+ ]++ , referenceTest "can get references to data constructor"+ ("References.hs", 13, 2)+ YesIncludeDeclaration+ [ ("References.hs", 13, 2)+ , ("References.hs", 16, 14)+ , ("References.hs", 19, 21)+ ]++ , referenceTest "getting references works in the other module"+ ("OtherModule.hs", 6, 0)+ YesIncludeDeclaration+ [ ("OtherModule.hs", 6, 0)+ , ("OtherModule.hs", 8, 16)+ ]++ , referenceTest "getting references works in the Main module"+ ("Main.hs", 9, 0)+ YesIncludeDeclaration+ [ ("Main.hs", 9, 0)+ , ("Main.hs", 10, 4)+ ]++ , referenceTest "getting references to main works"+ ("Main.hs", 5, 0)+ YesIncludeDeclaration+ [ ("Main.hs", 4, 0)+ , ("Main.hs", 5, 0)+ ]++ , referenceTest "can get type references"+ ("Main.hs", 9, 9)+ YesIncludeDeclaration+ [ ("Main.hs", 9, 0)+ , ("Main.hs", 9, 9)+ , ("Main.hs", 10, 0)+ ]++ , expectFailBecause "references provider does not respect includeDeclaration parameter" $+ referenceTest "works when we ask to exclude declarations"+ ("References.hs", 4, 7)+ NoExcludeDeclaration+ [ ("References.hs", 6, 0)+ , ("References.hs", 6, 14)+ , ("References.hs", 9, 7)+ , ("References.hs", 10, 11)+ ]++ , referenceTest "INCORRECTLY returns declarations when we ask to exclude them"+ ("References.hs", 4, 7)+ NoExcludeDeclaration+ [ ("References.hs", 4, 6)+ , ("References.hs", 6, 0)+ , ("References.hs", 6, 14)+ , ("References.hs", 9, 7)+ , ("References.hs", 10, 11)+ ]+ ]++ , testGroup "can get references to non FOIs"+ [ referenceTest "can get references to symbol defined in a module we import"+ ("References.hs", 22, 4)+ YesIncludeDeclaration+ [ ("References.hs", 22, 4)+ , ("OtherModule.hs", 0, 20)+ , ("OtherModule.hs", 4, 0)+ ]++ , referenceTest "can get references in modules that import us to symbols we define"+ ("OtherModule.hs", 4, 0)+ YesIncludeDeclaration+ [ ("References.hs", 22, 4)+ , ("OtherModule.hs", 0, 20)+ , ("OtherModule.hs", 4, 0)+ ]++ , referenceTest "can get references to symbol defined in a module we import transitively"+ ("References.hs", 24, 4)+ YesIncludeDeclaration+ [ ("References.hs", 24, 4)+ , ("OtherModule.hs", 0, 48)+ , ("OtherOtherModule.hs", 2, 0)+ ]++ , referenceTest "can get references in modules that import us transitively to symbols we define"+ ("OtherOtherModule.hs", 2, 0)+ YesIncludeDeclaration+ [ ("References.hs", 24, 4)+ , ("OtherModule.hs", 0, 48)+ , ("OtherOtherModule.hs", 2, 0)+ ]++ , referenceTest "can get type references to other modules"+ ("Main.hs", 12, 10)+ YesIncludeDeclaration+ [ ("Main.hs", 12, 7)+ , ("Main.hs", 13, 0)+ , ("References.hs", 12, 5)+ , ("References.hs", 16, 0)+ ]+ ]+ ]++-- | When we ask for all references to symbol "foo", should the declaration "foo+-- = 2" be among the references returned?+data IncludeDeclaration =+ YesIncludeDeclaration+ | NoExcludeDeclaration++getReferences' :: SymbolLocation -> IncludeDeclaration -> Session ([Location])+getReferences' (file, l, c) includeDeclaration = do+ doc <- openDoc file "haskell"+ getReferences doc (Position l c) $ toBool includeDeclaration+ where toBool YesIncludeDeclaration = True+ toBool NoExcludeDeclaration = False++referenceTestSession :: String -> FilePath -> [FilePath] -> (FilePath -> Session ()) -> TestTree+referenceTestSession name thisDoc docs' f = testSessionWithExtraFiles "references" name $ \dir -> do+ -- needed to build whole project indexing+ configureCheckProject True+ let docs = map (dir </>) $ delete thisDoc $ nubOrd docs'+ -- Initial Index+ docid <- openDoc thisDoc "haskell"+ let+ loop :: [FilePath] -> Session ()+ loop [] = pure ()+ loop docs = do+ doc <- skipManyTill anyMessage $ referenceReady (`elem` docs)+ loop (delete doc docs)+ loop docs+ f dir+ closeDoc docid++-- | Given a location, lookup the symbol and all references to it. Make sure+-- they are the ones we expect.+referenceTest :: String -> SymbolLocation -> IncludeDeclaration -> [SymbolLocation] -> TestTree+referenceTest name loc includeDeclaration expected =+ referenceTestSession name (fst3 loc) docs $ \dir -> do+ actual <- getReferences' loc includeDeclaration+ liftIO $ actual `expectSameLocations` map (first3 (dir </>)) expected+ where+ docs = map fst3 expected++type SymbolLocation = (FilePath, UInt, UInt)++expectSameLocations :: [Location] -> [SymbolLocation] -> Assertion+expectSameLocations actual expected = do+ let actual' =+ Set.map (\location -> (location ^. L.uri+ , location ^. L.range . L.start . L.line . Lens.to fromIntegral+ , location ^. L.range . L.start . L.character . Lens.to fromIntegral))+ $ Set.fromList actual+ expected' <- Set.fromList <$>+ (forM expected $ \(file, l, c) -> do+ fp <- canonicalizePath file+ return (filePathToUri fp, l, c))+ actual' @?= expected'
+ test/exe/RootUriTests.hs view
@@ -0,0 +1,26 @@++module RootUriTests (tests) where++import Control.Monad.IO.Class (liftIO)+import Development.IDE.GHC.Util+import Development.IDE.Test (expectNoMoreDiagnostics)+import Language.LSP.Test+import System.FilePath+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils+++-- | checks if we use InitializeParams.rootUri for loading session+tests :: TestTree+tests = testCase "use rootUri" . runTest "dirA" "dirB" $ \dir -> do+ let bPath = dir </> "dirB/Foo.hs"+ liftIO $ copyTestDataFiles dir "rootUri"+ bSource <- liftIO $ readFileUtf8 bPath+ _ <- createDoc "Foo.hs" "haskell" bSource+ expectNoMoreDiagnostics 0.5+ where+ -- similar to run' except we can configure where to start ghcide and session+ runTest :: FilePath -> FilePath -> (FilePath -> Session ()) -> IO ()+ runTest dir1 dir2 s = withTempDir $ \dir -> runInDir' dir dir1 dir2 [] (s dir)
+ test/exe/SafeTests.hs view
@@ -0,0 +1,38 @@++module SafeTests (tests) where++import qualified Data.Text as T+import Development.IDE.Test (expectNoMoreDiagnostics)+import Language.LSP.Test++import Test.Tasty+import TestUtils++tests :: TestTree+tests =+ testGroup+ "SafeHaskell"+ [ -- Test for https://github.com/haskell/ghcide/issues/424+ testSessionWait "load" $ do+ let sourceA =+ T.unlines+ ["{-# LANGUAGE Trustworthy #-}"+ ,"module A where"+ ,"import System.IO.Unsafe"+ ,"import System.IO ()"+ ,"trustWorthyId :: a -> a"+ ,"trustWorthyId i = unsafePerformIO $ do"+ ," putStrLn \"I'm safe\""+ ," return i"]+ sourceB =+ T.unlines+ ["{-# LANGUAGE Safe #-}"+ ,"module B where"+ ,"import A"+ ,"safeId :: a -> a"+ ,"safeId = trustWorthyId"+ ]++ _ <- createDoc "A.hs" "haskell" sourceA+ _ <- createDoc "B.hs" "haskell" sourceB+ expectNoMoreDiagnostics 1 ]
+ test/exe/SymlinkTests.hs view
@@ -0,0 +1,27 @@++module SymlinkTests (tests) where++import Control.Monad.IO.Class (liftIO)+import Development.IDE.Test (expectDiagnosticsWithTags)+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+import System.Directory+import System.FilePath++import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++-- | Tests for projects that use symbolic links one way or another+tests :: TestTree+tests =+ testGroup "Projects using Symlinks"+ [ testCase "Module is symlinked" $ runWithExtraFiles "symlink" $ \dir -> do+ liftIO $ createFileLink (dir </> "some_loc" </> "Sym.hs") (dir </> "other_loc" </> "Sym.hs")+ let fooPath = dir </> "src" </> "Foo.hs"+ _ <- openDoc fooPath "haskell"+ expectDiagnosticsWithTags [("src" </> "Foo.hs", [(DiagnosticSeverity_Warning, (2, 0), "The import of 'Sym' is redundant", Just DiagnosticTag_Unnecessary)])]+ pure ()+ ]
+ test/exe/THTests.hs view
@@ -0,0 +1,194 @@++{-# LANGUAGE OverloadedLabels #-}++module THTests (tests) where++import Control.Monad.IO.Class (liftIO)+import Data.Row+import qualified Data.Text as T+import Development.IDE.GHC.Util+import Development.IDE.Test (expectCurrentDiagnostics,+ expectDiagnostics,+ expectNoMoreDiagnostics)+import Language.LSP.Protocol.Types hiding (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..), mkRange)+import Language.LSP.Test+import System.FilePath+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests =+ testGroup+ "TemplateHaskell"+ [ -- Test for https://github.com/haskell/ghcide/pull/212+ testSessionWait "load" $ do+ let sourceA =+ T.unlines+ [ "{-# LANGUAGE PackageImports #-}",+ "{-# LANGUAGE TemplateHaskell #-}",+ "module A where",+ "import \"template-haskell\" Language.Haskell.TH",+ "a :: Integer",+ "a = $(litE $ IntegerL 3)"+ ]+ sourceB =+ T.unlines+ [ "{-# LANGUAGE PackageImports #-}",+ "{-# LANGUAGE TemplateHaskell #-}",+ "module B where",+ "import A",+ "import \"template-haskell\" Language.Haskell.TH",+ "b :: Integer",+ "b = $(litE $ IntegerL $ a) + n"+ ]+ _ <- createDoc "A.hs" "haskell" sourceA+ _ <- createDoc "B.hs" "haskell" sourceB+ expectDiagnostics [ ( "B.hs", [(DiagnosticSeverity_Error, (6, 29), "Variable not in scope: n")] ) ]+ , testSessionWait "newtype-closure" $ do+ let sourceA =+ T.unlines+ [ "{-# LANGUAGE DeriveDataTypeable #-}"+ ,"{-# LANGUAGE TemplateHaskell #-}"+ ,"module A (a) where"+ ,"import Data.Data"+ ,"import Language.Haskell.TH"+ ,"newtype A = A () deriving (Data)"+ ,"a :: ExpQ"+ ,"a = [| 0 |]"]+ let sourceB =+ T.unlines+ [ "{-# LANGUAGE TemplateHaskell #-}"+ ,"module B where"+ ,"import A"+ ,"b :: Int"+ ,"b = $( a )" ]+ _ <- createDoc "A.hs" "haskell" sourceA+ _ <- createDoc "B.hs" "haskell" sourceB+ return ()+ , thReloadingTest False+ , thLoadingTest+ , thCoreTest+ , ignoreInWindowsBecause "Broken in windows" $ thReloadingTest True+ -- Regression test for https://github.com/haskell/haskell-language-server/issues/891+ , thLinkingTest False+ , ignoreInWindowsBecause "Broken in windows" $ thLinkingTest True+ , testSessionWait "findsTHIdentifiers" $ do+ let sourceA =+ T.unlines+ [ "{-# LANGUAGE TemplateHaskell #-}"+ , "module A (a) where"+ , "import Language.Haskell.TH (ExpQ)"+ , "a :: ExpQ" -- TH 2.17 requires an explicit type signature since splices are polymorphic+ , "a = [| glorifiedID |]"+ , "glorifiedID :: a -> a"+ , "glorifiedID = id" ]+ let sourceB =+ T.unlines+ [ "{-# OPTIONS_GHC -Wall #-}"+ , "{-# LANGUAGE TemplateHaskell #-}"+ , "module B where"+ , "import A"+ , "main = $a (putStrLn \"success!\")"]+ _ <- createDoc "A.hs" "haskell" sourceA+ _ <- createDoc "B.hs" "haskell" sourceB+ expectDiagnostics [ ( "B.hs", [(DiagnosticSeverity_Warning, (4, 0), "Top-level binding with no type signature: main :: IO ()")] ) ]+ , testCase "findsTHnewNameConstructor" $ runWithExtraFiles "THNewName" $ \dir -> do++ -- This test defines a TH value with the meaning "data A = A" in A.hs+ -- Loads and export the template in B.hs+ -- And checks wether the constructor A can be loaded in C.hs+ -- This test does not fail when either A and B get manually loaded before C.hs+ -- or when we remove the seemingly unnecessary TH pragma from C.hs++ let cPath = dir </> "C.hs"+ _ <- openDoc cPath "haskell"+ expectDiagnostics [ ( cPath, [(DiagnosticSeverity_Warning, (3, 0), "Top-level binding with no type signature: a :: A")] ) ]+ ]+++-- | Test that all modules have linkables+thLoadingTest :: TestTree+thLoadingTest = testCase "Loading linkables" $ runWithExtraFiles "THLoading" $ \dir -> do+ let thb = dir </> "THB.hs"+ _ <- openDoc thb "haskell"+ expectNoMoreDiagnostics 1++thCoreTest :: TestTree+thCoreTest = testCase "Verifying TH core files" $ runWithExtraFiles "THCoreFile" $ \dir -> do+ let thc = dir </> "THC.hs"+ _ <- openDoc thc "haskell"+ expectNoMoreDiagnostics 1++-- | test that TH is reevaluated on typecheck+thReloadingTest :: Bool -> TestTree+thReloadingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do++ let aPath = dir </> "THA.hs"+ bPath = dir </> "THB.hs"+ cPath = dir </> "THC.hs"++ aSource <- liftIO $ readFileUtf8 aPath -- th = [d|a = ()|]+ bSource <- liftIO $ readFileUtf8 bPath -- $th+ cSource <- liftIO $ readFileUtf8 cPath -- c = a :: ()++ adoc <- createDoc aPath "haskell" aSource+ bdoc <- createDoc bPath "haskell" bSource+ cdoc <- createDoc cPath "haskell" cSource++ expectDiagnostics [("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")])]++ -- Change th from () to Bool+ let aSource' = T.unlines $ init (T.lines aSource) ++ ["th_a = [d| a = False|]"]+ changeDoc adoc [TextDocumentContentChangeEvent . InR . (.==) #text $ aSource']+ -- generate an artificial warning to avoid timing out if the TH change does not propagate+ changeDoc cdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ cSource <> "\nfoo=()"]++ -- Check that the change propagates to C+ expectDiagnostics+ [("THC.hs", [(DiagnosticSeverity_Error, (4, 4), "Couldn't match expected type '()' with actual type 'Bool'")])+ ,("THC.hs", [(DiagnosticSeverity_Warning, (6,0), "Top-level binding")])+ ,("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level bindin")])+ ]++ closeDoc adoc+ closeDoc bdoc+ closeDoc cdoc+ where+ name = "reloading-th-test" <> if unboxed then "-unboxed" else ""+ dir | unboxed = "THUnboxed"+ | otherwise = "TH"++thLinkingTest :: Bool -> TestTree+thLinkingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do++ let aPath = dir </> "THA.hs"+ bPath = dir </> "THB.hs"++ aSource <- liftIO $ readFileUtf8 aPath -- th_a = [d|a :: ()|]+ bSource <- liftIO $ readFileUtf8 bPath -- $th_a++ adoc <- createDoc aPath "haskell" aSource+ bdoc <- createDoc bPath "haskell" bSource++ expectDiagnostics [("THB.hs", [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")])]++ let aSource' = T.unlines $ init (init (T.lines aSource)) ++ ["th :: DecsQ", "th = [d| a = False|]"]+ changeDoc adoc [TextDocumentContentChangeEvent . InR . (.==) #text $ aSource']++ -- modify b too+ let bSource' = T.unlines $ init (T.lines bSource) ++ ["$th"]+ changeDoc bdoc [TextDocumentContentChangeEvent . InR . (.==) #text $ bSource']+ waitForProgressBegin+ waitForAllProgressDone++ expectCurrentDiagnostics bdoc [(DiagnosticSeverity_Warning, (4,thDollarIdx), "Top-level binding")]++ closeDoc adoc+ closeDoc bdoc+ where+ name = "th-linking-test" <> if unboxed then "-unboxed" else ""+ dir | unboxed = "THUnboxed"+ | otherwise = "TH"
+ test/exe/TestUtils.hs view
@@ -0,0 +1,329 @@++{-# LANGUAGE GADTs #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE TypeOperators #-}++module TestUtils where++import Control.Applicative.Combinators+import Control.Concurrent.Async+import Control.Exception (bracket_, finally)+import Control.Lens ((.~))+import qualified Control.Lens as Lens+import qualified Control.Lens.Extras as Lens+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.Foldable+import Data.Function ((&))+import Data.Maybe+import qualified Data.Text as T+import Development.IDE.GHC.Compat (GhcVersion (..), ghcVersion)+import Development.IDE.GHC.Util+import qualified Development.IDE.Main as IDE+import Development.IDE.Test (canonicalizeUri,+ configureCheckProject,+ expectNoMoreDiagnostics)+import Development.IDE.Test.Runfiles+import Development.IDE.Types.Location+import Development.Shake (getDirectoryFilesIO)+import Ide.Logger (Recorder, WithPriority,+ cmapWithPrio)+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.Directory+import System.Environment.Blank (getEnv, setEnv, unsetEnv)+import System.FilePath+import System.Info.Extra (isMac, isWindows)+import qualified System.IO.Extra+import System.Process.Extra (createPipe)+import Test.Tasty+import Test.Tasty.ExpectedFailure+import Test.Tasty.HUnit++import LogType++-- | Wait for the next progress begin step+waitForProgressBegin :: Session ()+waitForProgressBegin = skipManyTill anyMessage $ satisfyMaybe $ \case+ FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) | Lens.is _workDoneProgressBegin v-> Just ()+ _ -> Nothing++-- | Wait for the first progress end step+-- Also implemented in hls-test-utils Test.Hls+waitForProgressDone :: Session ()+waitForProgressDone = skipManyTill anyMessage $ satisfyMaybe $ \case+ FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) | Lens.is _workDoneProgressEnd v -> Just ()+ _ -> Nothing++-- | Wait for all progress to be done+-- Needs at least one progress done notification to return+-- Also implemented in hls-test-utils Test.Hls+waitForAllProgressDone :: Session ()+waitForAllProgressDone = loop+ where+ loop = do+ ~() <- skipManyTill anyMessage $ satisfyMaybe $ \case+ FromServerMess SMethod_Progress (TNotificationMessage _ _ (ProgressParams _ v)) |Lens.is _workDoneProgressEnd v-> Just ()+ _ -> Nothing+ done <- null <$> getIncompleteProgressSessions+ unless done loop++run :: Session a -> IO a+run s = run' (const s)++run' :: (FilePath -> Session a) -> IO a+run' s = withTempDir $ \dir -> runInDir dir (s dir)++runInDir :: FilePath -> Session a -> IO a+runInDir dir = runInDir' dir "." "." []++-- | Takes a directory as well as relative paths to where we should launch the executable as well as the session root.+runInDir' :: FilePath -> FilePath -> FilePath -> [String] -> Session a -> IO a+runInDir' = runInDir'' lspTestCaps++runInDir''+ :: ClientCapabilities+ -> FilePath+ -> FilePath+ -> FilePath+ -> [String]+ -> Session b+ -> IO b+runInDir'' lspCaps dir startExeIn startSessionIn extraOptions s = do++ ghcideExe <- locateGhcideExecutable+ let startDir = dir </> startExeIn+ let projDir = dir </> startSessionIn++ createDirectoryIfMissing True startDir+ createDirectoryIfMissing True projDir+ -- Temporarily hack around https://github.com/mpickering/hie-bios/pull/56+ -- since the package import test creates "Data/List.hs", which otherwise has no physical home+ createDirectoryIfMissing True $ projDir ++ "/Data"++ shakeProfiling <- getEnv "SHAKE_PROFILING"+ let cmd = unwords $+ [ghcideExe, "--lsp", "--test", "--verify-core-file", "--verbose", "-j2", "--cwd", startDir+ ] ++ ["--shake-profiling=" <> dir | Just dir <- [shakeProfiling]+ ] ++ extraOptions+ -- HIE calls getXgdDirectory which assumes that HOME is set.+ -- Only sets HOME if it wasn't already set.+ setEnv "HOME" "/homeless-shelter" False+ conf <- getConfigFromEnv+ runSessionWithConfig conf cmd lspCaps projDir $ do+ configureCheckProject False+ s++-- | Version of 'System.IO.Extra.withTempDir' that canonicalizes the path+-- Which we need to do on macOS since the $TMPDIR can be in @/private/var@ or+-- @/var@+withTempDir :: (FilePath -> IO a) -> IO a+withTempDir f = System.IO.Extra.withTempDir $ \dir -> do+ dir' <- canonicalizePath dir+ f dir'++lspTestCaps :: ClientCapabilities+lspTestCaps = fullCaps { _window = Just $ WindowClientCapabilities (Just True) Nothing Nothing }++getConfigFromEnv :: IO SessionConfig+getConfigFromEnv = do+ logColor <- fromMaybe True <$> checkEnv "LSP_TEST_LOG_COLOR"+ timeoutOverride <- fmap read <$> getEnv "LSP_TIMEOUT"+ return defaultConfig+ { messageTimeout = fromMaybe (messageTimeout defaultConfig) timeoutOverride+ , logColor+ }+ where+ checkEnv :: String -> IO (Maybe Bool)+ checkEnv s = fmap convertVal <$> getEnv s+ convertVal "0" = False+ convertVal _ = True++testSessionWait :: HasCallStack => String -> Session () -> TestTree+testSessionWait name = testSession name .+ -- Check that any diagnostics produced were already consumed by the test case.+ --+ -- If in future we add test cases where we don't care about checking the diagnostics,+ -- this could move elsewhere.+ --+ -- Experimentally, 0.5s seems to be long enough to wait for any final diagnostics to appear.+ ( >> expectNoMoreDiagnostics 0.5)++testSession :: String -> Session () -> TestTree+testSession name = testCase name . run++xfail :: TestTree -> String -> TestTree+xfail = flip expectFailBecause++ignoreInWindowsBecause :: String -> TestTree -> TestTree+ignoreInWindowsBecause = ignoreFor (BrokenForOS Windows)++ignoreInWindowsForGHC810 :: TestTree -> TestTree+ignoreInWindowsForGHC810 =+ ignoreFor (BrokenSpecific Windows [GHC810]) "tests are unreliable in windows for ghc 8.10"++ignoreForGHC92Plus :: String -> TestTree -> TestTree+ignoreForGHC92Plus = ignoreFor (BrokenForGHC [GHC92, GHC94, GHC96])++knownBrokenForGhcVersions :: [GhcVersion] -> String -> TestTree -> TestTree+knownBrokenForGhcVersions ghcVers = knownBrokenFor (BrokenForGHC ghcVers)++data BrokenOS = Linux | MacOS | Windows deriving (Show)++data IssueSolution = Broken | Ignore deriving (Show)++data BrokenTarget =+ BrokenSpecific BrokenOS [GhcVersion]+ -- ^Broken for `BrokenOS` with `GhcVersion`+ | BrokenForOS BrokenOS+ -- ^Broken for `BrokenOS`+ | BrokenForGHC [GhcVersion]+ -- ^Broken for `GhcVersion`+ deriving (Show)++-- | Ignore test for specific os and ghc with reason.+ignoreFor :: BrokenTarget -> String -> TestTree -> TestTree+ignoreFor = knownIssueFor Ignore++-- | Known broken for specific os and ghc with reason.+knownBrokenFor :: BrokenTarget -> String -> TestTree -> TestTree+knownBrokenFor = knownIssueFor Broken++-- | Deal with `IssueSolution` for specific OS and GHC.+knownIssueFor :: IssueSolution -> BrokenTarget -> String -> TestTree -> TestTree+knownIssueFor solution = go . \case+ BrokenSpecific bos vers -> isTargetOS bos && isTargetGhc vers+ BrokenForOS bos -> isTargetOS bos+ BrokenForGHC vers -> isTargetGhc vers+ where+ isTargetOS = \case+ Windows -> isWindows+ MacOS -> isMac+ Linux -> not isWindows && not isMac++ isTargetGhc = elem ghcVersion++ go True = case solution of+ Broken -> expectFailBecause+ Ignore -> ignoreTestBecause+ go False = \_ -> id++data Expect+ = ExpectRange Range -- Both gotoDef and hover should report this range+ | ExpectLocation Location+-- | ExpectDefRange Range -- Only gotoDef should report this range+ | ExpectHoverRange Range -- Only hover should report this range+ | ExpectHoverText [T.Text] -- the hover message must contain these snippets+ | ExpectHoverExcludeText [T.Text] -- the hover message must _not_ contain these snippets+ | ExpectHoverTextRegex T.Text -- the hover message must match this pattern+ | ExpectExternFail -- definition lookup in other file expected to fail+ | ExpectNoDefinitions+ | ExpectNoHover+-- | ExpectExtern -- TODO: as above, but expected to succeed: need some more info in here, once we have some working examples+ deriving Eq++mkR :: UInt -> UInt -> UInt -> UInt -> Expect+mkR startLine startColumn endLine endColumn = ExpectRange $ mkRange startLine startColumn endLine endColumn++mkL :: Uri -> UInt -> UInt -> UInt -> UInt -> Expect+mkL uri startLine startColumn endLine endColumn = ExpectLocation $ Location uri $ mkRange startLine startColumn endLine endColumn++++testSessionWithExtraFiles :: FilePath -> String -> (FilePath -> Session ()) -> TestTree+testSessionWithExtraFiles prefix name = testCase name . runWithExtraFiles prefix++testSession' :: String -> (FilePath -> Session ()) -> TestTree+testSession' name = testCase name . run'++++mkRange :: UInt -> UInt -> UInt -> UInt -> Range+mkRange a b c d = Range (Position a b) (Position c d)+++runWithExtraFiles :: FilePath -> (FilePath -> Session a) -> IO a+runWithExtraFiles prefix s = withTempDir $ \dir -> do+ copyTestDataFiles dir prefix+ runInDir dir (s dir)++copyTestDataFiles :: FilePath -> FilePath -> IO ()+copyTestDataFiles dir prefix = do+ -- Copy all the test data files to the temporary workspace+ testDataFiles <- getDirectoryFilesIO ("test/data" </> prefix) ["//*"]+ for_ testDataFiles $ \f -> do+ createDirectoryIfMissing True $ dir </> takeDirectory f+ copyFile ("test/data" </> prefix </> f) (dir </> f)++withLongTimeout :: IO a -> IO a+withLongTimeout = bracket_ (setEnv "LSP_TIMEOUT" "120" True) (unsetEnv "LSP_TIMEOUT")++++lspTestCapsNoFileWatches :: ClientCapabilities+lspTestCapsNoFileWatches = lspTestCaps & L.workspace . Lens._Just . L.didChangeWatchedFiles .~ Nothing++openTestDataDoc :: FilePath -> Session TextDocumentIdentifier+openTestDataDoc path = do+ source <- liftIO $ readFileUtf8 $ "test/data" </> path+ createDoc path "haskell" source++pattern R :: UInt -> UInt -> UInt -> UInt -> Range+pattern R x y x' y' = Range (Position x y) (Position x' y')++checkDefs :: Definition |? ([DefinitionLink] |? Null) -> Session [Expect] -> Session ()+checkDefs (defToLocation -> defs) mkExpectations = traverse_ check =<< mkExpectations where+ check (ExpectRange expectedRange) = do+ assertNDefinitionsFound 1 defs+ assertRangeCorrect (head defs) expectedRange+ check (ExpectLocation expectedLocation) = do+ assertNDefinitionsFound 1 defs+ liftIO $ do+ canonActualLoc <- canonicalizeLocation (head defs)+ canonExpectedLoc <- canonicalizeLocation expectedLocation+ canonActualLoc @?= canonExpectedLoc+ check ExpectNoDefinitions = do+ assertNDefinitionsFound 0 defs+ check ExpectExternFail = liftIO $ assertFailure "Expecting to fail to find in external file"+ check _ = pure () -- all other expectations not relevant to getDefinition++ assertNDefinitionsFound :: Int -> [a] -> Session ()+ assertNDefinitionsFound n defs = liftIO $ assertEqual "number of definitions" n (length defs)++ assertRangeCorrect Location{_range = foundRange} expectedRange =+ liftIO $ expectedRange @=? foundRange++canonicalizeLocation :: Location -> IO Location+canonicalizeLocation (Location uri range) = Location <$> canonicalizeUri uri <*> pure range++defToLocation :: Definition |? ([DefinitionLink] |? Null) -> [Location]+defToLocation (InL (Definition (InL l))) = [l]+defToLocation (InL (Definition (InR ls))) = ls+defToLocation (InR (InL defLink)) = (\(DefinitionLink LocationLink{_targetUri,_targetRange}) -> Location _targetUri _targetRange) <$> defLink+defToLocation (InR (InR Null)) = []++-- | Ghc 9 doesn't include the $-sign in TH warnings like earlier versions did+thDollarIdx :: UInt+thDollarIdx | ghcVersion >= GHC90 = 1+ | otherwise = 0++testIde :: Recorder (WithPriority Log) -> IDE.Arguments -> Session () -> IO ()+testIde recorder arguments session = do+ config <- getConfigFromEnv+ cwd <- getCurrentDirectory+ (hInRead, hInWrite) <- createPipe+ (hOutRead, hOutWrite) <- createPipe+ let projDir = "."+ let server = IDE.defaultMain (cmapWithPrio LogIDEMain recorder) arguments+ { IDE.argsHandleIn = pure hInRead+ , IDE.argsHandleOut = pure hOutWrite+ }++ flip finally (setCurrentDirectory cwd) $ withAsync server $ \_ ->+ runSessionWithHandles hInWrite hOutRead config lspTestCaps projDir session
+ test/exe/UnitTests.hs view
@@ -0,0 +1,110 @@++module UnitTests (tests) where++import Control.Concurrent+import Control.Monad.IO.Class (liftIO)+import Data.IORef+import Data.IORef.Extra (atomicModifyIORef_)+import Data.List.Extra+import Data.String (IsString (fromString))+import qualified Data.Text as T+import Development.IDE.Core.FileStore (getModTime)+import qualified Development.IDE.Main as IDE+import qualified Development.IDE.Plugin.HLS.GhcIde as Ghcide+import qualified Development.IDE.Types.Diagnostics as Diagnostics+import Development.IDE.Types.Location+import qualified FuzzySearch+import Ide.Logger (Logger, Recorder,+ WithPriority, cmapWithPrio)+import Ide.PluginUtils (pluginDescToIdePlugins)+import Ide.Types+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import LogType (Log (..))+import Network.URI+import qualified Progress+import System.IO.Extra hiding (withTempDir)+import System.Mem (performGC)+import Test.Tasty+import Test.Tasty.ExpectedFailure+import Test.Tasty.HUnit+import TestUtils+import Text.Printf (printf)++tests :: Recorder (WithPriority Log) -> Logger -> TestTree+tests recorder logger = do+ testGroup "Unit"+ [ testCase "empty file path does NOT work with the empty String literal" $+ uriToFilePath' (fromNormalizedUri $ filePathToUri' "") @?= Just "."+ , testCase "empty file path works using toNormalizedFilePath'" $+ uriToFilePath' (fromNormalizedUri $ filePathToUri' (toNormalizedFilePath' "")) @?= Just ""+ , testCase "empty path URI" $ do+ Just URI{..} <- pure $ parseURI (T.unpack $ getUri $ fromNormalizedUri emptyPathUri)+ uriScheme @?= "file:"+ uriPath @?= ""+ , testCase "from empty path URI" $ do+ let uri = Uri "file://"+ uriToFilePath' uri @?= Just ""+ , testCase "showDiagnostics prints ranges 1-based (like vscode)" $ do+ let diag = ("", Diagnostics.ShowDiag, Diagnostic+ { _codeDescription = Nothing+ , _data_ = Nothing+ , _range = Range+ { _start = Position{_line = 0, _character = 1}+ , _end = Position{_line = 2, _character = 3}+ }+ , _severity = Nothing+ , _code = Nothing+ , _source = Nothing+ , _message = ""+ , _relatedInformation = Nothing+ , _tags = Nothing+ })+ let shown = T.unpack (Diagnostics.showDiagnostics [diag])+ let expected = "1:2-3:4"+ assertBool (unwords ["expected to find range", expected, "in diagnostic", shown]) $+ expected `isInfixOf` shown+ , testCase "notification handlers run in priority order" $ do+ orderRef <- newIORef []+ let plugins = pluginDescToIdePlugins $+ [ (priorityPluginDescriptor i)+ { pluginNotificationHandlers = mconcat+ [ mkPluginNotificationHandler SMethod_TextDocumentDidOpen $ \_ _ _ _ ->+ liftIO $ atomicModifyIORef_ orderRef (i:)+ ]+ }+ | i <- [1..20]+ ] ++ Ghcide.descriptors (cmapWithPrio LogGhcIde recorder)+ priorityPluginDescriptor i = (defaultPluginDescriptor $ fromString $ show i){pluginPriority = i}++ testIde recorder (IDE.testing (cmapWithPrio LogIDEMain recorder) logger plugins) $ do+ _ <- createDoc "A.hs" "haskell" "module A where"+ waitForProgressDone+ actualOrder <- liftIO $ reverse <$> readIORef orderRef++ -- Handlers are run in priority descending order+ liftIO $ actualOrder @?= [20, 19 .. 1]+ , ignoreTestBecause "The test fails sometimes showing 10000us" $+ testCase "timestamps have millisecond resolution" $ do+ resolution_us <- findResolution_us 1+ let msg = printf "Timestamps do not have millisecond resolution: %dus" resolution_us+ assertBool msg (resolution_us <= 1000)+ , Progress.tests+ , FuzzySearch.tests+ ]++findResolution_us :: Int -> IO Int+findResolution_us delay_us | delay_us >= 1000000 = error "Unable to compute timestamp resolution"+findResolution_us delay_us = withTempFile $ \f -> withTempFile $ \f' -> do+ performGC+ writeFile f ""+ threadDelay delay_us+ writeFile f' ""+ t <- getModTime f+ t' <- getModTime f'+ if t /= t' then return delay_us else findResolution_us (delay_us * 10)
+ test/exe/WatchedFileTests.hs view
@@ -0,0 +1,83 @@++{-# LANGUAGE GADTs #-}+{-# LANGUAGE PolyKinds #-}++module WatchedFileTests (tests) where++import Control.Applicative.Combinators+import Control.Monad.IO.Class (liftIO)+import qualified Data.Aeson as A+import qualified Data.Text as T+import Development.IDE.Test (expectDiagnostics)+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types hiding+ (SemanticTokenAbsolute (..),+ SemanticTokenRelative (..),+ SemanticTokensEdit (..),+ mkRange)+import Language.LSP.Test+import System.Directory+import System.FilePath+-- import Test.QuickCheck.Instances ()+import Test.Tasty+import Test.Tasty.HUnit+import TestUtils++tests :: TestTree+tests = testGroup "watched files"+ [ testGroup "Subscriptions"+ [ testSession' "workspace files" $ \sessionDir -> do+ liftIO $ writeFile (sessionDir </> "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"WatchedFilesMissingModule\"]}}"+ _doc <- createDoc "A.hs" "haskell" "{-#LANGUAGE NoImplicitPrelude #-}\nmodule A where\nimport WatchedFilesMissingModule"+ watchedFileRegs <- getWatchedFilesSubscriptionsUntil SMethod_TextDocumentPublishDiagnostics++ -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle+ liftIO $ length watchedFileRegs @?= 2++ , testSession' "non workspace file" $ \sessionDir -> do+ tmpDir <- liftIO getTemporaryDirectory+ let yaml = "cradle: {direct: {arguments: [\"-i" <> tail(init(show tmpDir)) <> "\", \"A\", \"WatchedFilesMissingModule\"]}}"+ liftIO $ writeFile (sessionDir </> "hie.yaml") yaml+ _doc <- createDoc "A.hs" "haskell" "{-# LANGUAGE NoImplicitPrelude#-}\nmodule A where\nimport WatchedFilesMissingModule"+ watchedFileRegs <- getWatchedFilesSubscriptionsUntil SMethod_TextDocumentPublishDiagnostics++ -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle+ liftIO $ length watchedFileRegs @?= 2++ -- TODO add a test for didChangeWorkspaceFolder+ ]+ , testGroup "Changes"+ [+ testSession' "workspace files" $ \sessionDir -> do+ liftIO $ writeFile (sessionDir </> "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"B\"]}}"+ liftIO $ writeFile (sessionDir </> "B.hs") $ unlines+ ["module B where"+ ,"b :: Bool"+ ,"b = False"]+ _doc <- createDoc "A.hs" "haskell" $ T.unlines+ ["module A where"+ ,"import B"+ ,"a :: ()"+ ,"a = b"+ ]+ expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 4), "Couldn't match expected type '()' with actual type 'Bool'")])]+ -- modify B off editor+ liftIO $ writeFile (sessionDir </> "B.hs") $ unlines+ ["module B where"+ ,"b :: Int"+ ,"b = 0"]+ sendNotification SMethod_WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $+ [FileEvent (filePathToUri $ sessionDir </> "B.hs") FileChangeType_Changed ]+ expectDiagnostics [("A.hs", [(DiagnosticSeverity_Error, (3, 4), "Couldn't match expected type '()' with actual type 'Int'")])]+ ]+ ]++getWatchedFilesSubscriptionsUntil :: forall m. SServerMethod m -> Session [DidChangeWatchedFilesRegistrationOptions]+getWatchedFilesSubscriptionsUntil m = do+ msgs <- manyTill (Just <$> message SMethod_ClientRegisterCapability <|> Nothing <$ anyMessage) (message m)+ return+ [ x+ | Just TRequestMessage{_params = RegistrationParams regs} <- msgs+ , Registration _id "workspace/didChangeWatchedFiles" (Just args) <- regs+ , Just x@(DidChangeWatchedFilesRegistrationOptions _) <- [A.decode . A.encode $ args]+ ]
test/src/Development/IDE/Test.hs view
@@ -43,20 +43,20 @@ import Data.Default import qualified Data.Map.Strict as Map import Data.Maybe (fromJust)+import Data.Proxy import Data.Text (Text) import qualified Data.Text as T import Development.IDE.Plugin.Test (TestRequest (..), WaitForIdeRuleResult, ideResultSuccess) import Development.IDE.Test.Diagnostic+import GHC.TypeLits ( symbolVal ) import Ide.Plugin.Config (CheckParents, checkProject)+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types import Language.LSP.Test hiding (message) import qualified Language.LSP.Test as LspTest-import Language.LSP.Types hiding- (SemanticTokenAbsolute (length, line),- SemanticTokenRelative (length),- SemanticTokensEdit (_start))-import Language.LSP.Types.Lens as Lsp import System.Directory (canonicalizePath) import System.FilePath (equalFilePath) import System.Time.Extra@@ -75,23 +75,23 @@ -- if any diagnostic messages arrive in that period expectNoMoreDiagnostics :: HasCallStack => Seconds -> Session () expectNoMoreDiagnostics timeout =- expectMessages STextDocumentPublishDiagnostics timeout $ \diagsNot -> do- let fileUri = diagsNot ^. params . uri- actual = diagsNot ^. params . diagnostics- unless (actual == List []) $ liftIO $+ expectMessages SMethod_TextDocumentPublishDiagnostics timeout $ \diagsNot -> do+ let fileUri = diagsNot ^. L.params . L.uri+ actual = diagsNot ^. L.params . L.diagnostics+ unless (actual == []) $ liftIO $ assertFailure $ "Got unexpected diagnostics for " <> show fileUri <> " got " <> show actual -expectMessages :: SMethod m -> Seconds -> (ServerMessage m -> Session ()) -> Session ()+expectMessages :: SMethod m -> Seconds -> (TServerMessage m -> Session ()) -> Session () expectMessages m timeout handle = do -- Give any further diagnostic messages time to arrive. liftIO $ sleep timeout -- Send a dummy message to provoke a response from the server. -- This guarantees that we have at least one message to -- process, so message won't block or timeout.- let cm = SCustomMethod "test"+ let cm = SMethod_CustomMethod (Proxy @"test") i <- sendRequest cm $ A.toJSON GetShakeSessionQueueCount go cm i where@@ -102,7 +102,7 @@ flushMessages :: Session () flushMessages = do- let cm = SCustomMethod "non-existent-method"+ let cm = SMethod_CustomMethod (Proxy @"non-existent-method") i <- sendRequest cm A.Null void (responseForId cm i) <|> ignoreOthers cm i where@@ -118,8 +118,8 @@ = expectDiagnosticsWithTags . map (second (map (\(ds, c, t) -> (ds, c, t, Nothing)))) -unwrapDiagnostic :: NotificationMessage TextDocumentPublishDiagnostics -> (Uri, List Diagnostic)-unwrapDiagnostic diagsNot = (diagsNot^.params.uri, diagsNot^.params.diagnostics)+unwrapDiagnostic :: TServerMessage Method_TextDocumentPublishDiagnostics -> (Uri, [Diagnostic])+unwrapDiagnostic diagsNot = (diagsNot^. L.params . L.uri, diagsNot^. L.params . L.diagnostics) expectDiagnosticsWithTags :: HasCallStack => [(String, [(DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)])] -> Session () expectDiagnosticsWithTags expected = do@@ -130,13 +130,13 @@ expectDiagnosticsWithTags' :: (HasCallStack, MonadIO m) =>- m (Uri, List Diagnostic) ->+ m (Uri, [Diagnostic]) -> Map.Map NormalizedUri [(DiagnosticSeverity, Cursor, T.Text, Maybe DiagnosticTag)] -> m () expectDiagnosticsWithTags' next m | null m = do (_,actual) <- next case actual of- List [] ->+ [] -> return () _ -> liftIO $ assertFailure $ "Got unexpected diagnostics:" <> show actual@@ -178,19 +178,19 @@ checkDiagnosticsForDoc TextDocumentIdentifier {_uri} expected obtained = do let expected' = Map.fromList [(nuri, map (\(ds, c, t) -> (ds, c, t, Nothing)) expected)] nuri = toNormalizedUri _uri- expectDiagnosticsWithTags' (return (_uri, List obtained)) expected'+ expectDiagnosticsWithTags' (return (_uri, obtained)) expected' canonicalizeUri :: Uri -> IO Uri canonicalizeUri uri = filePathToUri <$> canonicalizePath (fromJust (uriToFilePath uri)) -diagnostic :: Session (NotificationMessage TextDocumentPublishDiagnostics)-diagnostic = LspTest.message STextDocumentPublishDiagnostics+diagnostic :: Session (TNotificationMessage Method_TextDocumentPublishDiagnostics)+diagnostic = LspTest.message SMethod_TextDocumentPublishDiagnostics tryCallTestPlugin :: (A.FromJSON b) => TestRequest -> Session (Either ResponseError b) tryCallTestPlugin cmd = do- let cm = SCustomMethod "test"+ let cm = SMethod_CustomMethod (Proxy @"test") waitId <- sendRequest cm (A.toJSON cmd)- ResponseMessage{_result} <- skipManyTill anyMessage $ responseForId cm waitId+ TResponseMessage{_result} <- skipManyTill anyMessage $ responseForId cm waitId return $ case _result of Left e -> Left e Right json -> case A.fromJSON json of@@ -230,8 +230,8 @@ waitForCustomMessage :: T.Text -> (A.Value -> Maybe res) -> Session res waitForCustomMessage msg pred = skipManyTill anyMessage $ satisfyMaybe $ \case- FromServerMess (SCustomMethod lbl) (NotMess NotificationMessage{_params = value})- | lbl == msg -> pred value+ FromServerMess (SMethod_CustomMethod p) (NotMess TNotificationMessage{_params = value})+ | symbolVal p == T.unpack msg -> pred value _ -> Nothing waitForGC :: Session [T.Text]@@ -242,7 +242,7 @@ configureCheckProject :: Bool -> Session () configureCheckProject overrideCheckProject =- sendNotification SWorkspaceDidChangeConfiguration+ sendNotification SMethod_WorkspaceDidChangeConfiguration (DidChangeConfigurationParams $ toJSON def{checkProject = overrideCheckProject}) @@ -252,9 +252,10 @@ referenceReady :: (FilePath -> Bool) -> Session FilePath referenceReady pred = satisfyMaybe $ \case- FromServerMess (SCustomMethod "ghcide/reference/ready") (NotMess NotificationMessage{_params})+ FromServerMess (SMethod_CustomMethod p) (NotMess TNotificationMessage{_params}) | A.Success fp <- A.fromJSON _params , pred fp+ , symbolVal p == "ghcide/reference/ready" -> Just fp _ -> Nothing
test/src/Development/IDE/Test/Diagnostic.hs view
@@ -1,10 +1,10 @@ module Development.IDE.Test.Diagnostic where -import Control.Lens ((^.))-import qualified Data.Text as T-import GHC.Stack (HasCallStack)-import Language.LSP.Types-import Language.LSP.Types.Lens as Lsp+import Control.Lens ((^.))+import qualified Data.Text as T+import GHC.Stack (HasCallStack)+import Language.LSP.Protocol.Lens+import Language.LSP.Protocol.Types -- | (0-based line number, 0-based column number) type Cursor = (UInt, UInt)@@ -33,10 +33,10 @@ standardizeQuotes (T.toLower $ d ^. message) && hasTag expectedTag (d ^. tags) - hasTag :: Maybe DiagnosticTag -> Maybe (List DiagnosticTag) -> Bool- hasTag Nothing _ = True- hasTag (Just _) Nothing = False- hasTag (Just actualTag) (Just (List tags)) = actualTag `elem` tags+ hasTag :: Maybe DiagnosticTag -> Maybe [DiagnosticTag] -> Bool+ hasTag Nothing _ = True+ hasTag (Just _) Nothing = False+ hasTag (Just actualTag) (Just tags) = actualTag `elem` tags standardizeQuotes :: T.Text -> T.Text standardizeQuotes msg = let