packages feed

ghc-tags-1.12: src/GhcTags/Tag.hs

module GhcTags.Tag
  ( -- * Tag
    TagType (..)
  , SingTagType (..)
  , Tag (..)
  , ETag
  , CTag
  , ETagMap
  , CTagMap

    -- ** Tag fields
  , TagName (..)
  , TagFileName (..)
  , ExCommand (..)
  , TagAddress (..)
  , CTagAddress
  , ETagAddress
  , TagKind (..)
  , CTagKind
  , ETagKind
  , TagDefinition (..)
  , TagFields (..)
  , CTagFields
  , ETagFields
  , TagField (..)

    -- ** Ordering and combining tags
  , compareTags

    -- * Create 'Tag' from a 'GhcTag'
  , ghcTagToTag
  ) where

import Control.DeepSeq
import Data.Function (on)
import Data.Map.Strict (Map)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import GHC.Data.FastString (bytesFS)
-- GHC imports
import GHC.Driver.Session (DynFlags, initSDocContext)
import GHC.Types.SrcLoc
  ( SrcSpan (..)
  , srcSpanFile
  , srcSpanStartLine
  )
import GHC.Utils.Outputable qualified as Out
import Yamlet qualified as Y

import GhcTags.Ghc
  ( GhcTag (..)
  , GhcTagKind (..)
  )

--
-- Tag
--

-- | Promoted data type used to disntinguish 'CTag's from 'ETag's.
data TagType = CTag | ETag
  deriving stock (Show)

-- | Singletons for promoted types.
data SingTagType (tt :: TagType) where
  SingCTag :: SingTagType 'CTag
  SingETag :: SingTagType 'ETag

-- | 'ByteString' which encodes a tag name.
newtype TagName = TagName {getTagName :: Text}
  deriving stock (Eq, Ord, Show)

instance NFData TagName where
  rnf = rnf . getTagName

-- | 'ByteString' which encodes a tag file name.
newtype TagFileName = TagFileName {getTagFileName :: Text}
  deriving stock (Eq, Ord, Show)
  deriving newtype (Y.FromYaml, Y.ToYaml)

instance NFData TagFileName where
  rnf = rnf . getTagFileName

-- | When we parse a `tags` file we can eithera find no kind or recognize the
-- kind of GhcTagKind or we store the found character kind.  This allows us to
-- preserve information from parsed tags files which were not created by
-- `ghc-tags-plugin'
--
-- 'GhcTags.CTag.Utils.tagKindToChar' and 'GhcTags.CTag.Utils.charToTagKind'
-- define the character of each kind.
data TagKind (tt :: TagType) where
  TkModule :: TagKind tt
  TkTerm :: TagKind tt
  TkFunction :: TagKind tt
  TkTypeConstructor :: TagKind tt
  TkDataConstructor :: TagKind tt
  TkGADTConstructor :: TagKind tt
  TkRecordField :: TagKind tt
  TkTypeSynonym :: TagKind tt
  TkTypeSignature :: TagKind tt
  TkPatternSynonym :: TagKind tt
  TkTypeClass :: TagKind tt
  TkTypeClassMember :: TagKind tt
  TkTypeClassInstance :: TagKind tt
  TkTypeFamily :: TagKind tt
  TkTypeFamilyInstance :: TagKind tt
  TkDataTypeFamily :: TagKind tt
  TkDataTypeFamilyInstance :: TagKind tt
  TkForeignImport :: TagKind tt
  TkForeignExport :: TagKind tt
  CharKind :: Char -> TagKind tt
  NoKind :: TagKind tt

instance NFData (TagKind tt) where
  rnf x = x `seq` ()

type CTagKind = TagKind 'CTag
type ETagKind = TagKind 'ETag

deriving stock instance Eq (TagKind tt)
deriving stock instance Ord (TagKind tt)
deriving stock instance Show (TagKind tt)

newtype ExCommand = ExCommand {getExCommand :: Text}
  deriving stock (Eq, Ord, Show)

-- | Tag address, either from a parsed file or from Haskell's AST>
data TagAddress (tt :: TagType) where
  -- | Address of an etag is a line number and byte offset from the begining
  -- of the file.
  TagLineOff :: Int -> Int -> TagAddress 'ETag
  -- | ctags can only use range ex-commands as an address (or a sequence of
  -- them separated by `;`). We parse line number specifically, since they
  -- are useful for ordering tags.
  TagLine :: Int -> TagAddress 'CTag
  -- | A tag address can be just an ex command.
  TagCommand :: ExCommand -> TagAddress 'CTag

instance NFData (TagAddress tt) where
  rnf x = x `seq` ()

-- | 'CTag' addresses.
type CTagAddress = TagAddress 'CTag

-- | 'ETag' addresses.
type ETagAddress = TagAddress 'ETag

deriving stock instance Eq (TagAddress tt)
deriving stock instance Ord (TagAddress tt)
deriving stock instance Show (TagAddress tt)

-- | Emacs tags specific field.
data TagDefinition (tt :: TagType) where
  TagDefinition :: Text -> TagDefinition 'ETag
  NoTagDefinition :: TagDefinition tt

instance NFData (TagDefinition tt) where
  rnf x = x `seq` ()

deriving stock instance Show (TagDefinition tt)
deriving stock instance Eq (TagDefinition tt)
deriving stock instance Ord (TagDefinition tt)

-- | Unit of data associated with a tag.  Vim natively supports `file:` and
-- `kind:` tags but it can display any other tags too.
data TagField = TagField
  { fieldName :: Text
  , fieldValue :: Text
  }
  deriving stock (Eq, Ord, Show)

instance NFData TagField where
  rnf x = x `seq` ()

-- | File field; tags which contain 'fileField' are called static (aka static
-- in @C@), such tags are only visible in the current file)
fileField :: TagField
fileField = TagField {fieldName = "file", fieldValue = ""}

-- | Ctags specific list of 'TagField's.
data TagFields (tt :: TagType) where
  NoTagFields :: TagFields 'ETag
  TagFields
    :: [TagField]
    -> TagFields 'CTag

instance NFData (TagFields tt) where
  rnf NoTagFields = ()
  rnf (TagFields fs) = rnf fs

deriving stock instance Show (TagFields tt)
deriving stock instance Eq (TagFields tt)
deriving stock instance Ord (TagFields tt)
instance Semigroup (TagFields tt) where
  NoTagFields <> NoTagFields = NoTagFields
  (TagFields a) <> (TagFields b) = TagFields (a ++ b)
instance Monoid (TagFields 'CTag) where
  mempty = TagFields mempty
instance Monoid (TagFields 'ETag) where
  mempty = NoTagFields

type CTagFields = TagFields 'CTag
type ETagFields = TagFields 'ETag

-- | Tag record.  For either ctags or etags formats.  It is either filled with
-- information parsed from a tags file or from *GHC* ast.
data Tag (tt :: TagType) = Tag
  { tagName :: TagName
  -- ^ name of the tag
  , tagKind :: TagKind tt
  -- ^ ctags specifc field, which classifies tags
  , tagAddr :: TagAddress tt
  -- ^ address in source file
  , tagDefinition :: TagDefinition tt
  -- ^ etags specific field; only tags read from emacs tags file contain this
  -- field.
  , tagFields :: TagFields tt
  -- ^ ctags specific field
  }
  deriving stock (Show, Eq, Ord)

instance NFData (Tag tt) where
  rnf Tag {..} =
    rnf tagName
      `seq` rnf tagKind
      `seq` rnf tagAddr
      `seq` rnf tagDefinition
      `seq` rnf tagFields

type CTag = Tag 'CTag
type ETag = Tag 'ETag

type TagMap tt = Map TagFileName [Tag tt]
type CTagMap = TagMap 'CTag
type ETagMap = TagMap 'ETag

-- | Total order relation on 'Tag' elements.
--
-- It sorts type classes / type families ('TkTypeClass', 'TkTypeFamily',
-- 'TkDataTypeFamily')  before instances ('TkTypeClassInstance',
-- 'TkTypeFamilyInstance', 'TkDataTypeFamilyInstance'); but also (as a side
-- effect of keeping transitivity property) it will put type classes and their
-- instances before other kinds.
--
-- It satisfies the following properties:
--
-- * anti-symmetry
-- * reflexivity
-- * transitivity
-- * partial consistency with 'Eq' instance:
--
--   prop> a == b => compareTags a b == EQ
compareTags :: forall (tt :: TagType). Ord (TagAddress tt) => Tag tt -> Tag tt -> Ordering
compareTags t0 t1 =
  on compare tagName t0 t1
    -- sort type classes / type families before their instances,
    -- and take precendence over a file where they are defined.
    --
    -- This will also sort type classes and instances before any
    -- other terms.
    <> on compare getTkClass t0 t1
    <> on compare tagAddr t0 t1
    <> on compare tagKind t0 t1
  where
    getTkClass :: Tag tt -> Maybe (TagKind tt)
    getTkClass t = case tagKind t of
      TkTypeClass -> Just TkTypeClass
      TkTypeClassInstance -> Just TkTypeClassInstance
      TkTypeFamily -> Just TkTypeFamily
      TkTypeFamilyInstance -> Just TkTypeFamilyInstance
      TkDataTypeFamily -> Just TkDataTypeFamily
      TkDataTypeFamilyInstance -> Just TkDataTypeFamilyInstance
      _ -> Nothing

--
--  GHC interface
--

-- | Create a 'Tag' from 'GhcTag'.
ghcTagToTag :: SingTagType tt -> DynFlags -> GhcTag -> Maybe (TagFileName, Tag tt)
ghcTagToTag sing dynFlags GhcTag {gtSrcSpan, gtTag, gtKind, gtIsExported, gtFFI} =
  case gtSrcSpan of
    UnhelpfulSpan {} -> Nothing
    RealSrcSpan realSrcSpan _ ->
      Just . (fileName realSrcSpan,) $
        Tag
          { tagName = TagName tagName
          , tagAddr = case sing of
              SingETag -> TagLineOff (srcSpanStartLine realSrcSpan) 0
              SingCTag -> TagLine (srcSpanStartLine realSrcSpan)
          , tagKind = fromGhcTagKind gtKind
          , tagDefinition = NoTagDefinition
          , tagFields =
              ( staticField
                  <> ffiField
                  <> kindField
              )
                sing
          }
  where
    -- A file name is not necessarily valid UTF-8, so a strict decoder would
    -- throw and take the whole run down.
    fileName = TagFileName . Text.decodeUtf8Lenient . bytesFS . srcSpanFile

    tagName = Text.decodeUtf8Lenient gtTag

    fromGhcTagKind :: GhcTagKind -> TagKind tt
    fromGhcTagKind = \case
      GtkModule -> TkModule
      GtkTerm -> TkTerm
      GtkFunction -> TkFunction
      GtkTypeConstructor {} -> TkTypeConstructor
      GtkDataConstructor {} -> TkDataConstructor
      GtkGADTConstructor {} -> TkGADTConstructor
      GtkRecordField -> TkRecordField
      GtkTypeSynonym {} -> TkTypeSynonym
      GtkTypeSignature {} -> TkTypeSignature
      GtkTypeKindSignature {} -> TkTypeSignature
      GtkPatternSynonym -> TkPatternSynonym
      GtkTypeClass -> TkTypeClass
      GtkTypeClassMember {} -> TkTypeClassMember
      GtkTypeClassInstance {} -> TkTypeClassInstance
      GtkTypeFamily {} -> TkTypeFamily
      GtkTypeFamilyInstance {} -> TkTypeFamilyInstance
      GtkDataTypeFamily {} -> TkDataTypeFamily
      GtkDataTypeFamilyInstance {} -> TkDataTypeFamilyInstance
      GtkForeignImport -> TkForeignImport
      GtkForeignExport -> TkForeignExport

    -- static field (wheather term is exported or not)
    staticField :: SingTagType tt -> TagFields tt
    staticField = \case
      SingETag -> NoTagFields
      SingCTag ->
        TagFields $
          if gtIsExported
            then mempty
            else [fileField]

    -- ffi field
    ffiField :: SingTagType tt -> TagFields tt
    ffiField = \case
      SingETag -> NoTagFields
      SingCTag ->
        TagFields $
          case gtFFI of
            Nothing -> mempty
            Just ffi -> [TagField "ffi" $ Text.decodeUtf8Lenient ffi]

    -- 'TagFields' from 'GhcTagKind'
    kindField :: SingTagType tt -> TagFields tt
    kindField = \case
      SingETag -> NoTagFields
      SingCTag ->
        case gtKind of
          GtkTypeClassInstance hsType ->
            mkField "instance" hsType
          GtkTypeFamily (Just hsKind) ->
            mkField kindFieldName hsKind
          GtkDataTypeFamily (Just hsKind) ->
            mkField kindFieldName hsKind
          GtkTypeSignature hsSigWcType ->
            mkField typeFieldName hsSigWcType
          GtkTypeSynonym hsType ->
            mkField typeFieldName hsType
          GtkTypeConstructor (Just hsKind) ->
            mkField kindFieldName hsKind
          GtkDataConstructor decl ->
            TagFields
              [ TagField
                  { fieldName = termFieldName
                  , fieldValue = render decl
                  }
              ]
          GtkGADTConstructor hsType ->
            mkField typeFieldName hsType
          _ -> mempty

    kindFieldName, typeFieldName, termFieldName :: Text
    kindFieldName = "Kind" -- "kind" is reserverd
    typeFieldName = "type"
    termFieldName = "term"

    --
    -- fields
    --

    mkField :: Out.Outputable p => Text -> p -> TagFields 'CTag
    mkField fieldName p =
      TagFields
        [ TagField
            { fieldName
            , fieldValue = render p
            }
        ]

    render :: Out.Outputable p => p -> Text
    render hsType =
      Text.intercalate " " -- remove all line breaks, tabs and multiple spaces
        . Text.words
        . Text.pack
        $ Out.renderWithContext
          ( initSDocContext
              dynFlags
              ( Out.setStyleColoured False $
                  Out.mkErrStyle Out.neverQualify
              )
          )
          (Out.ppr hsType)