tadka-2.0.0.0: test/props/Tabs.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Tab-stop expansion (post-v1 hardening): tabs in rendered source lines
-- expand to the next tab stop, and carets align under the /expanded/ source.
-- The alignment property is the mathematical statement of the fix: a caret's
-- leading-space count equals the display width of the tab-expanded source that
-- precedes the span.
module Tabs (group) where
import Data.Text (Text)
import qualified Data.Text as T
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Tadka.Internal.Renderer.Graphical (caretLayout)
import Tadka.Internal.Width (displayColumnAt, expandTabs, textWidth)
group :: Group
group = Group "Tab-aware source rendering"
[ ("expandTabs leaves no tab characters", prop_noTabs)
, ("expanded width == displayColumnAt of line", prop_widthConsistent)
, ("displayColumnAt is monotonic", prop_monotonic)
, ("a tab always lands on a tab stop", prop_tabStop)
, ("caret aligns under the expanded source", prop_alignment)
, ("caret layout stays non-negative with tabs", prop_nonNeg)
]
-- Unicode-hard text that also includes tabs.
genScalar :: Gen Char
genScalar = Gen.frequency
[ (4, Gen.filterT (\c -> c /= '\n') (Gen.enum ' ' '~'))
, (3, pure '\t')
, (2, Gen.enum '\x0300' '\x036F') -- combining marks (width 0)
, (2, Gen.enum '\x4E00' '\x4E30') -- CJK (width 2)
, (1, Gen.enum '\x1F600' '\x1F610') -- emoji (width 2)
]
genLine :: Gen Text
genLine = Gen.text (Range.linear 0 40) genScalar
genTabW :: Gen Int
genTabW = Gen.int (Range.linear 1 8)
prop_noTabs :: Property
prop_noTabs = property $ do
tw <- forAll genTabW
line <- forAll genLine
assert (not (T.any (== '\t') (expandTabs tw line)))
prop_widthConsistent :: Property
prop_widthConsistent = property $ do
tw <- forAll genTabW
line <- forAll genLine
textWidth (expandTabs tw line) === displayColumnAt tw line (T.length line)
prop_monotonic :: Property
prop_monotonic = property $ do
tw <- forAll genTabW
line <- forAll genLine
let n = T.length line
a <- forAll (Gen.int (Range.linear 0 n))
b <- forAll (Gen.int (Range.linear 0 n))
let (lo, hi) = (min a b, max a b)
assert (displayColumnAt tw line lo <= displayColumnAt tw line hi)
-- The defining property of a tab stop: the column immediately after a tab is a
-- multiple of the tab width.
prop_tabStop :: Property
prop_tabStop = property $ do
tw <- forAll genTabW
line <- forAll genLine
let tabIdxs = [ i | (i, c) <- zip [0 ..] (T.unpack line), c == '\t' ]
mapM_ (\i -> displayColumnAt tw line (i + 1) `mod` tw === 0) tabIdxs
-- THE alignment guarantee: the caret's display offset equals the width of the
-- tab-expanded source preceding the span, so the caret sits under it exactly.
prop_alignment :: Property
prop_alignment = property $ do
tw <- forAll genTabW
line <- forAll genLine
let n = T.length line
startCol <- forAll (Gen.int (Range.linear 1 (n + 1)))
spanLen <- forAll (Gen.int (Range.linear 0 (n + 5)))
let (dispStart, _) = caretLayout tw line startCol spanLen
dispStart === textWidth (expandTabs tw (T.take (startCol - 1) line))
prop_nonNeg :: Property
prop_nonNeg = property $ do
tw <- forAll genTabW
line <- forAll genLine
let n = T.length line
startCol <- forAll (Gen.int (Range.linear 1 (n + 1)))
spanLen <- forAll (Gen.int (Range.linear 0 (n + 5)))
let (dispStart, caretWidth) = caretLayout tw line startCol spanLen
assert (dispStart >= 0)
assert (caretWidth >= 1)