packages feed

tilia-0.0.1.0: src/Tilia/Span.hs

-- | Regions of the input, and the questions asked about them.
--
-- Deliberately not GHC's @RealSrcSpan@. Almost everything here wants to ask
-- one of a handful of questions—did this occupy a single line, did that
-- begin on the line this ended on, was there an empty line between them—and
-- a type of our own keeps the modules that ask them free of the compiler's
-- libraries. Conversion happens at the edge, in "Tilia.Span.Ghc", which is
-- the only place that has to know what GHC's positions look like.
module Tilia.Span
  ( -- * Spans
    Span (..),
    mkSpan,

    -- * Asking about one
    isSingleLine,

    -- * Asking about two
    sameLine,
    blankBetween,
    meets,
    covers,

    -- * Narrowing
    startOf,
    endOf,

    -- * Positions
    startPoint,
    endPoint,
  )
where

-- | A region of the input.
data Span = Span
  { spanStartLine :: !Int,
    spanStartColumn :: !Int,
    spanEndLine :: !Int,
    spanEndColumn :: !Int
  }
  deriving (Eq, Ord, Show)

-- | Build a 'Span' from start and end positions, each a line and a column.
mkSpan :: (Int, Int) -> (Int, Int) -> Span
mkSpan (sl, sc) (el, ec) = Span sl sc el ec

-- | The smallest span covering both arguments.
--
-- Printing code needs this often enough—a construct the syntax tree has no
-- single node for still has to be laid out as a unit—that it is worth having
-- as an instance rather than as a function each caller reimplements.
instance Semigroup Span where
  a <> b =
    Span
      { spanStartLine = min (spanStartLine a) (spanStartLine b),
        spanStartColumn = case compare (spanStartLine a) (spanStartLine b) of
          LT -> spanStartColumn a
          GT -> spanStartColumn b
          EQ -> min (spanStartColumn a) (spanStartColumn b),
        spanEndLine = max (spanEndLine a) (spanEndLine b),
        spanEndColumn = case compare (spanEndLine a) (spanEndLine b) of
          GT -> spanEndColumn a
          LT -> spanEndColumn b
          EQ -> max (spanEndColumn a) (spanEndColumn b)
      }

-- | Did this occupy a single line of the input?
--
-- The question the whole formatter turns on: what was written on one line
-- stays on one line, and what was spread out stays spread out.
isSingleLine :: Span -> Bool
isSingleLine s = spanStartLine s == spanEndLine s

-- | Did the second thing begin on the line the first thing ended on?
--
-- This is the question behind almost every hanging decision: a body may only
-- hang off what precedes it when the author had them starting together.
sameLine :: Maybe Span -> Maybe Span -> Bool
sameLine (Just a) (Just b) = spanEndLine a == spanStartLine b
sameLine _ _ = False

-- | Was there an empty line between the two?
blankBetween :: Maybe Span -> Maybe Span -> Bool
blankBetween (Just a) (Just b) = spanStartLine b > spanEndLine a + 1
blankBetween _ _ = False

-- | Do the two cover any of the same input?
--
-- Touching counts: a span ending where the next begins shares that position,
-- and the callers that ask this are asking whether the two are looking at
-- one thing, not whether either strictly contains the other.
meets :: Span -> Span -> Bool
meets a b = startPoint a <= endPoint b && startPoint b <= endPoint a

-- | Does the first cover all of the second?
--
-- Reflexive, so a span covers itself. Callers wanting one thing to be
-- strictly inside another want this and inequality.
covers :: Span -> Span -> Bool
covers a b = startPoint a <= startPoint b && endPoint b <= endPoint a

-- | A zero-width span at the start of the given one.
startOf :: Span -> Span
startOf s = at (spanStartLine s, spanStartColumn s)

-- | A zero-width span at the end of the given one.
endOf :: Span -> Span
endOf s = at (spanEndLine s, spanEndColumn s)

at :: (Int, Int) -> Span
at position = mkSpan position position

-- | Where a span begins, as a position two of them may be compared by.
startPoint :: Span -> (Int, Int)
startPoint s = (spanStartLine s, spanStartColumn s)

-- | Where a span ends.
endPoint :: Span -> (Int, Int)
endPoint s = (spanEndLine s, spanEndColumn s)