typed-peg-0.5.0.0: src/PEG/Location.hs
{-# LANGUAGE BangPatterns #-}
-- | Lines and columns, computed from offsets.
--
-- The parser tracks only an offset — the number of characters consumed — and
-- 'PEG.Syntax.GetOffset' (@\@@ in the quasi-quoter) hands it to a semantic
-- action. Lines are not tracked while parsing, so a grammar that never asks
-- for a position pays nothing for them. Turning an offset into a line and a
-- column is done here, against the original input:
--
-- @
-- let idx = 'lineIndex' input -- once per input
-- 'lineCol' idx off -- O(log lines) per lookup
-- @
--
-- Lines and columns are both counted from 1. Every character, a tab
-- included, advances the column by one; this is the convention of most
-- editors' \"go to column\", not the tab-stop column "PEG.Indent" uses for
-- layout. On a 'Data.ByteString.ByteString' an offset counts bytes, as
-- everything else does; see "PEG.Stream".
module PEG.Location
( LineIndex
, lineIndex
, lineCol
, offsetToLineCol
) where
import qualified Data.IntMap.Strict as IM
import PEG.Stream (Stream (..))
-- | Where each line of an input starts: the offset of its first character,
-- mapped to its line number.
newtype LineIndex = LineIndex (IM.IntMap Int)
-- | Index the lines of an input. One pass over it.
lineIndex :: Stream s => s -> LineIndex
lineIndex s = LineIndex (IM.fromList ((0, 1) : starts))
where
-- The fold keeps the offset and line of the next character; a newline
-- starts a line at the offset after it.
(_, _, starts) = foldlS' step (0 :: Int, 1 :: Int, []) s
step (!off, !ln, acc) c
| c == '\n' = (off + 1, ln + 1, (off + 1, ln + 1) : acc)
| otherwise = (off + 1, ln, acc)
-- 'IM.fromList' rather than 'IM.fromDistinctAscList': the fold collects
-- the starts last first.
-- | The line and column, both from 1, of the character at an offset.
--
-- An offset past the end of the input is placed on the last line, as if that
-- line went on.
lineCol :: LineIndex -> Int -> (Int, Int)
lineCol (LineIndex m) off = case IM.lookupLE off m of
Just (start, ln) -> (ln, off - start + 1)
Nothing -> (1, off + 1) -- a negative offset; not produced by 'parse'
-- | @'lineCol' . 'lineIndex'@, for a single lookup. Indexing costs a pass
-- over the input, so build a 'LineIndex' once when there are many offsets to
-- convert.
offsetToLineCol :: Stream s => s -> Int -> (Int, Int)
offsetToLineCol s = lineCol (lineIndex s)