Skip to content

Implement explicit fixity in ghcide #2973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion ghcide/src/Development/IDE/Core/Actions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,24 @@ getAtPoint file pos = runMaybeT $ do
(hf, mapping) <- useE GetHieAst file
env <- hscEnv . fst <$> useE GhcSession file
dkMap <- lift $ maybe (DKMap mempty mempty) fst <$> runMaybeT (useE GetDocMap file)
tcGblEnv <- tmrTypechecked . fst <$> useE TypeCheck file
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using TypeCheck here is bad. It will make ghcide hovers slower, and it will break the ability to get hover information at cold startup from the local cache.

Now, if fixity was in a plugin, it would still create all the problems above since his-plugin-api collects all the results before sending the response. But one can:

  1. Disable the fixity plugin
  2. Implement partial responses

I think this is a blocker. Workarounds:

  1. Extract back to a plugin
  2. Introduce a new build rule for fixity information, and give it a persistent implementation with addPersistentRule. The persistent implementation can return some placeholder or you can cache the fixity info in hiedb if desired.


!pos' <- MaybeT (return $ fromCurrentPosition mapping pos)
MaybeT $ pure $ first (toCurrentRange mapping =<<) <$> AtPoint.atPoint opts hf dkMap env pos'

let pointContent = AtPoint.atPoint opts hf dkMap env pos'
fixityContent <- liftIO $ AtPoint.fixityAtPoint hf env tcGblEnv pos'

MaybeT $ pure $ first (toCurrentRange mapping =<<) <$> mergeContent pointContent fixityContent
where
-- | Respect point content first
mergeContent :: Maybe (Maybe Range, [T.Text]) -> Maybe (Maybe Range, [T.Text]) -> Maybe (Maybe Range, [T.Text])
mergeContent Nothing Nothing = Nothing
mergeContent x@Just{} Nothing = x
mergeContent Nothing x@Just{} = x
mergeContent (Just (r1, txt1)) (Just (r2, txt2)) =
if r1 == r2
then Just (r1, txt1 <> filter (not . T.null) txt2)
else Just (r1, txt1)

toCurrentLocations :: PositionMapping -> [Location] -> [Location]
toCurrentLocations mapping = mapMaybe go
Expand Down
14 changes: 10 additions & 4 deletions ghcide/src/Development/IDE/GHC/Compat/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ module Development.IDE.GHC.Compat.Core (
#endif
-- * Fixity
LexicalFixity(..),
Fixity(..),
mi_fix,
defaultFixity,
lookupFixityRn,
-- * ModSummary
ModSummary(..),
-- * HomeModInfo
Expand Down Expand Up @@ -378,6 +382,7 @@ module Development.IDE.GHC.Compat.Core (
module GHC.Types.Name.Cache,
module GHC.Types.Name.Env,
module GHC.Types.Name.Reader,
module GHC.Utils.Error,
#if MIN_VERSION_ghc(9,2,0)
module GHC.Types.Avail,
module GHC.Types.SourceFile,
Expand All @@ -388,7 +393,6 @@ module Development.IDE.GHC.Compat.Core (
module GHC.Types.Unique.Supply,
module GHC.Types.Var,
module GHC.Unit.Module,
module GHC.Utils.Error,
#else
module BasicTypes,
module Class,
Expand Down Expand Up @@ -552,6 +556,7 @@ import GHC.Runtime.Context (InteractiveImport (..))
import GHC.Parser.Lexer
import qualified GHC.Runtime.Linker as Linker
#endif
import GHC.Rename.Fixity (lookupFixityRn)
import GHC.Rename.Names
import GHC.Rename.Splice
import qualified GHC.Runtime.Interpreter as GHCi
Expand All @@ -568,7 +573,7 @@ import GHC.Tc.Utils.TcType as TcType
import qualified GHC.Types.Avail as Avail
#if MIN_VERSION_ghc(9,2,0)
import GHC.Types.Avail (greNamePrintableName)
import GHC.Types.Fixity (LexicalFixity (..))
import GHC.Types.Fixity (LexicalFixity (..), Fixity(..), defaultFixity)
#endif
#if MIN_VERSION_ghc(9,2,0)
import GHC.Types.Meta
Expand Down Expand Up @@ -613,11 +618,11 @@ import GHC.Unit.Module.Imported
import GHC.Unit.Module.ModDetails
import GHC.Unit.Module.ModGuts
import GHC.Unit.Module.ModIface (IfaceExport, ModIface (..),
ModIface_ (..))
ModIface_ (..), mi_fix)
import GHC.Unit.Module.ModSummary (ModSummary (..))
#endif
import GHC.Unit.State (ModuleOrigin (..))
import GHC.Utils.Error (Severity (..))
import GHC.Utils.Error (Severity (..), emptyMessages)
import GHC.Utils.Panic hiding (try)
import qualified GHC.Utils.Panic.Plain as Plain
#else
Expand Down Expand Up @@ -688,6 +693,7 @@ import qualified Panic as Plain
#endif
import Parser
import PatSyn
import RnFixity
#if MIN_VERSION_ghc(8,8,0)
import Plugins
#endif
Expand Down
48 changes: 47 additions & 1 deletion ghcide/src/Development/IDE/Spans/AtPoint.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
-- These are all pure functions that should execute quickly.
module Development.IDE.Spans.AtPoint (
atPoint
, fixityAtPoint
, gotoDefinition
, gotoTypeDefinition
, documentHighlight
Expand Down Expand Up @@ -49,8 +50,9 @@ import qualified Data.Text as T

import qualified Data.Array as A
import Data.Either
import Data.Either.Extra (eitherToMaybe)
import Data.List (isSuffixOf)
import Data.List.Extra (dropEnd1, nubOrd)
import Data.List.Extra (dropEnd1, nubOn, nubOrd)

import Data.Version (showVersion)
import Development.IDE.Types.Shake (WithHieDb)
Expand Down Expand Up @@ -203,6 +205,50 @@ gotoDefinition
gotoDefinition withHieDb getHieFile ideOpts imports srcSpans pos
= lift $ locationsAtPoint withHieDb getHieFile ideOpts imports pos srcSpans

fixityAtPoint
:: HieAstResult
-> HscEnv
-> TcGblEnv
-> Position
-> IO (Maybe (Maybe Range, [T.Text]))
fixityAtPoint (HAR _ hf _ _ _) env tcGblEnv pos = fmap listToMaybe $ sequence $ pointCommand hf pos fixityInfo
where
fixityInfo :: HieAST a -> IO (Maybe Range, [T.Text])
fixityInfo ast = do
let range = realSrcSpanToRange $ nodeSpan ast
names = mapMaybe eitherToMaybe $ M.keys $ getNodeIds ast
fixities <- getFixity names
pure (Just range, [toHoverContent fixities])

-- We use `handleGhcException` here to prevent hover crashed.
-- Because `runTcInteractive` may throw an exception if something
-- is wrong in it.
getFixity :: [Name] -> IO [(Name, Fixity)]
getFixity names =
liftIO
$ fmap (filter ((/= defaultFixity) . snd) . mapMaybe pickFixity)
$ forM names $ \name ->
(\(_, fixity) -> (name, fixity))
<$> Util.handleGhcException
(const $ pure (emptyMessages, Nothing))
(initTcWithGbl env tcGblEnv (realSrcLocSpan $ mkRealSrcLoc "<dummy>" 1 1) (lookupFixityRn name))
where
pickFixity :: (Name, Maybe Fixity) -> Maybe (Name, Fixity)
pickFixity (_, Nothing) = Nothing
pickFixity (name, Just f) = Just (name, f)

toHoverContent [] = ""
toHoverContent fixities =
-- We don't have much fixities on one position,
-- so `nubOn` is acceptable.
let contents = T.intercalate "\n\n" $ fixityText <$> nubOn snd fixities
contents' = "\n" <> sectionSeparator <> contents
in contents'

fixityText (name, Fixity _ precedence direction) =
printOutputable direction <> " " <> printOutputable precedence <> " `" <> printOutputable name <> "`"


-- | Synopsis for the name at a given position.
atPoint
:: IdeOptions
Expand Down
40 changes: 40 additions & 0 deletions ghcide/test/data/fixity/Fixity.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{-# LANGUAGE RecordWildCards #-}
module Fixity where
import Control.Monad
import Data.Function (on)
import Control.Applicative ((<|>))
f1 = (++)
f2 = ($)
f3 = (.)
f4 = (+)
f5 = 1 - 2
f6 = (<>)
f7 = (>>=)
f8 = (>=>)
f9 = elem
f10 = on
f11 = (||)
f12 = mod
f13 = (**)
f14 = (^)
f15 = (<$)
f16 = seq
f17 = (<|>)

infixr 7 >>:
infix 9 >>::
data F = G
{ (>>:) :: Int -> Int -> Int
, c :: Int
, (>>::) :: Char
}
f G{..} = undefined

infixl 1 `f`

infixr 9 >>>:
(>>>:) :: Int -> Int
(>>>:) x = 3

infixl 3 ~\:
(~\:) x y = 3
5 changes: 5 additions & 0 deletions ghcide/test/data/fixity/FixityImport.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module FixityImport where

import Fixity

g = (>>>:)
53 changes: 53 additions & 0 deletions ghcide/test/exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ main = do
, outlineTests
, highlightTests
, findDefinitionAndHoverTests
, hoverFixityTests
, pluginSimpleTests
, pluginParsedResultTests
, preprocessorTests
Expand Down Expand Up @@ -6523,6 +6524,58 @@ expectSameLocations actual expected = do
return (filePathToUri fp, l, c))
actual' @?= expected'

hoverFixityTests :: TestTree
hoverFixityTests = testGroup "Explicit fixity"
[ hoverTest "(++)" (Position 5 7) "infixr 5 `++`"
, hoverTest "($)" (Position 6 7) "infixr 0 `$`"
, hoverTest "(.)" (Position 7 7) "infixr 9 `.`"
, hoverTest "(+)" (Position 8 7) "infixl 6 `+`"
, hoverTest "(-)" (Position 9 8) "infixl 6 `-`"
, hoverTest "(<>)" (Position 10 7) "infixr 6 `<>`"
, hoverTest "(>>=)" (Position 11 7) "infixl 1 `>>=`"
, hoverTest "(>=>)" (Position 12 7) "infixr 1 `>=>`"
, hoverTest "elem" (Position 13 7) "infix 4 `elem`"
, hoverTest "on" (Position 14 7) "infixl 0 `on`"
, hoverTest "(||)" (Position 15 8) "infixr 2 `||`"
, hoverTest "mod" (Position 16 8) "infixl 7 `mod`"
, hoverTest "(**)" (Position 17 8) "infixr 8 `**`"
, hoverTest "(^)" (Position 18 8) "infixr 8 `^`"
, hoverTest "(<$)" (Position 19 8) "infixl 4 `<$`"
, hoverTest "seq" (Position 20 9) "infixr 0 `seq`"
, hoverTest "(<|>)" (Position 21 8) "infixl 3 `<|>`"
, hoverTest "fixity define" (Position 23 11) "infixr 7 `>>:`"
, hoverTest "record" (Position 28 10) "infix 9 `>>::`"
, hoverTest "wildcards" (Position 30 5) "infixr 7 `>>:`\n\ninfix 9 `>>::`"
, hoverTest "function" (Position 32 11) "infixl 1 `f`"
, hoverTest "signature" (Position 35 2) "infixr 9 `>>>:`"
, hoverTest "operator" (Position 36 2) "infixr 9 `>>>:`"
, hoverTest "escape" (Position 39 2) "infixl 3 `~\\:`"
, expectFail $ hoverTest "import" (Position 2 18) "Control.Monad***"
-- It will cause error like: "Failed to load interface for \8216Fixity\8217\nIt is not a module in the current program, or in any known package."
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get fixity from locally defined module failed because something like Failed to load interface for \8216Fixity\8217\nIt is not a module in the current program, or in any known package. It is strange.

It's a trivial case and I think we have an available function even without this.

-- while we look up fixities from imported local defined module, see test below.
, expectFailBecause "Not support yet"
$ hoverTestImport "imported" (Position 4 7) "infixr 9 `>>>:`"
]
where
hoverTest = hoverTest' "Fixity.hs"
hoverTestImport = hoverTest' "FixityImport.hs"

hoverTest' :: String -> TestName -> Position -> T.Text -> TestTree
hoverTest' docName title pos expected = testSessionWithExtraFiles "fixity" title $ \dir -> do
doc <- openDoc (dir </> docName) "haskell"
void $ waitForTypecheck doc
h <- getHover doc pos
let expected' = "\n" <> sectionSeparator <> expected
case h of
Nothing -> liftIO $ assertFailure "No hover"
Just (Hover contents _) -> case contents of
HoverContentsMS _ -> liftIO $ assertFailure "Unexpected content type"
HoverContents (MarkupContent _ txt) ->
liftIO
$ assertBool ("Failed to find " <> T.unpack expected <> " in " <> T.unpack txt)
$ expected' `T.isInfixOf` txt
closeDoc doc

----------------------------------------------------------------------
-- Utils
----------------------------------------------------------------------
Expand Down