Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
506f67b
Cover how a JBFL pattern matches a cursor
webdevred Aug 16, 2026
e4f6ef8
Cover prefix keys in the same pattern group
webdevred Aug 16, 2026
92e1166
Decide which of several matching patterns wins
webdevred Aug 16, 2026
471c4ae
Cover the literal array index and the ruleset merge
webdevred Aug 16, 2026
3b0c3ef
Put the rule semantics specs under Formatting.Rules
webdevred Aug 16, 2026
c299d8f
Updated Ord instance for NodePatternSelector
webdevred Aug 16, 2026
c049ac7
Moved Ord instance
webdevred Aug 16, 2026
2a9c631
Added rsHere and rsBelow
webdevred Aug 16, 2026
7d46f38
Fixed RuleSet type
webdevred Aug 16, 2026
171612a
More RuleSet fixes
webdevred Aug 16, 2026
f9791b9
Implemented new jbfl parsing logic
webdevred Aug 21, 2026
187c0a8
Build the matching specs from JBFL source
webdevred Aug 21, 2026
bffe5aa
Merge branch 'master' into rule-lookup-trie
webdevred Aug 21, 2026
645b9f8
Drop the pattern comparison the trie replaces
webdevred Aug 21, 2026
3c9397e
Walk the trie by breadcrumb in the rule lookup
webdevred Aug 21, 2026
2e5e577
Fixed lookupPropertyForCursor according to the new RuleSet
webdevred Aug 22, 2026
b2fa76c
Enabled -XPartialTypeSignatures in cabal.project.dev
webdevred Aug 22, 2026
e12ae10
Added all the ComplexNewLine properties to the prefix property list
webdevred Aug 22, 2026
1a27385
Added Indent to prefixProperties
webdevred Aug 22, 2026
e6a3a7b
Regenerated JBFL example AST
webdevred Aug 22, 2026
e3d4117
Adapt the matching specs to a lookup without MatchMode
webdevred Aug 22, 2026
9b57afa
Refactoring, removing redudant `lookupRule`
webdevred Aug 22, 2026
9c40072
Ran fourmolu
webdevred Aug 22, 2026
2cd00f6
Let PadDecimals and PreserveNumberFormat cascade again
webdevred Aug 22, 2026
a7b9ceb
Document how far each property reaches
webdevred Aug 22, 2026
0f0c2a1
Assert prefix precedence in both source orders
webdevred Aug 22, 2026
a21cfaf
Assert per-property merging across matching prefix rules
webdevred Aug 22, 2026
80de6eb
Assert the later of two rules with one pattern wins
webdevred Aug 22, 2026
42fb9a1
Fixed regressions
webdevred Aug 22, 2026
8b72d2e
Bugfix
webdevred Aug 22, 2026
52e9161
Drop NodePattern, unused since patterns stopped being keys
webdevred Aug 22, 2026
921b6bf
Use M.null and drop the duplicate NodePath import
webdevred Aug 22, 2026
f2446cb
Give the same-pattern spec a stable formatting
webdevred Aug 22, 2026
ec33792
Drop MatchMode, now decided by the property key
webdevred Aug 22, 2026
004dd8b
Regenerate the JBFL ASTs after the cascade change
webdevred Aug 22, 2026
048ff8f
Cut the comments back to the three that carry something
webdevred Aug 22, 2026
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
18 changes: 18 additions & 0 deletions JBFL_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ Typical use cases include:
- `TrailingComma: None` strips trailing commas. `Force` always adds them. `Preserve` (default) keeps whatever the source file had.
- `TrailingComma` is resolved at the child level, so `.* { TrailingComma: None; }` also strips the trailing comma in the root object.

### How far a property reaches

A pattern names one level. Some properties then apply only to what sits exactly
there, and others also apply to everything nested below it, so a rule on `.*`
can still decide how a value five levels down is printed.

| Reaches | Properties |
|-----------------------------|-------------------------------------------------------------------------------------------------|
| The matched value only | `AutoPad`, `AlignObjectKeys`, `AutoPadSubObjects` |
| The matched value and below | `PadDecimals`, `PadAmount`, `Indent`, `ComplexNewLine`, `TrailingComma`, `PreserveNumberFormat` |

The three in the first row all describe how one array or object lays out its own
children, so inheriting them would align structures the rule never mentioned.

This is why `.* { Indent: 2; }` indents the whole file while `.* { AutoPad: true; }`
only pads the root object, and why the shipped `minimal.jbfl` can set
`PadDecimals` on `.*.nodes[*][*]` or on a parent and get the same result.

## Detailed Rules Examples

### Pattern: `.*.nodes[*][*]`
Expand Down
3 changes: 2 additions & 1 deletion cabal.project.dev
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ package jbeam-edit
ghc-options:
-haddock
-Wno-invalid-haddock
-XPartialTypeSignatures

documentation: False
tests: True
flags: +dump-ast +transformation -windows-example-paths
flags: -modern-containers +dump-ast +transformation -windows-example-paths
665 changes: 304 additions & 361 deletions examples/ast/jbfl/complex.hs

Large diffs are not rendered by default.

516 changes: 248 additions & 268 deletions examples/ast/jbfl/minimal.hs

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions src/JbeamEdit/Core/NodePath.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ module JbeamEdit.Core.NodePath (
) where

import Data.Either.Extra (maybeToEither)
import Data.Function (on)
import Data.Sequence (Seq (..))
import Data.Text (Text)
import Data.Text qualified as T (isPrefixOf, show)
import Data.Text qualified as T (isPrefixOf, length, show)
import Data.Vector (Vector)
import Data.Vector qualified as V
import GHC.IsList (IsList (..))
Expand All @@ -29,7 +30,16 @@ data NodeSelector
| ObjectKey Text
| ObjectPrefixKey Text
| ObjectIndex Int
deriving (Eq, Ord, Read, Show)
deriving (Eq, Read, Show)

instance Ord NodeSelector where
compare = on compare rank
where
rank :: NodeSelector -> (Int, Int, Text)
rank (ObjectKey key) = (0, 0, key)
rank (ArrayIndex index) = (1, index, "")
rank (ObjectIndex i) = (2, i, "")
rank (ObjectPrefixKey prefix) = (3, negate (T.length prefix), prefix)

{- | node path
A NodePath is a Sequence of selectors to that point out a certain point in a Node tree, either to point at as something when fetching it from Node or to point to something compare that I at a certain point when doing updates.
Expand Down
24 changes: 11 additions & 13 deletions src/JbeamEdit/Formatting.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,12 @@ import JbeamEdit.Core.Node (
import JbeamEdit.Core.NodeCursor (newCursor)
import JbeamEdit.Core.NodeCursor qualified as NC
import JbeamEdit.Formatting.Rules (
MatchMode (..),
PropertyKey (..),
RuleSet (..),
applyPadLogic,
findPropertiesForCursor,
lookupProperty,
lookupPropertyForCursor,
lookupRule,
)
import JbeamEdit.Formatting.Rules.ComplexNewLine qualified as CNL
import JbeamEdit.Formatting.Rules.TrailingComma qualified as TC
Expand Down Expand Up @@ -238,7 +237,7 @@ addDelimiters rs index rowIdx c complexChildren state acc ns@((node, nodeHadComm
( \childCursor _ ->
fromMaybe
TC.Preserve
(lookupPropertyForCursor PrefixMatch TrailingComma rs childCursor)
(lookupPropertyForCursor TrailingComma rs childCursor)
)
index
node
Expand Down Expand Up @@ -320,17 +319,16 @@ doFormatNode
-> Text
doFormatNode rs cursor state elems =
let nodes = V.map fst elems
prefixProps = findPropertiesForCursor PrefixMatch cursor rs
exactProps = findPropertiesForCursor ExactMatch cursor rs
props = findPropertiesForCursor cursor rs

autoPadEnabled = lookupRule AutoPad exactProps == Just True
alignObjectKeysEnabled = lookupRule AlignObjectKeys exactProps == Just True
autopadSubObjectsEnabled = lookupRule AutoPadSubObjects exactProps == Just True
autoPadEnabled = lookupProperty AutoPad props == Just True
alignObjectKeysEnabled = lookupProperty AlignObjectKeys props == Just True
autopadSubObjectsEnabled = lookupProperty AutoPadSubObjects props == Just True

complexChildren =
lookupRule ComplexNewLine prefixProps == Just CNL.Force
lookupProperty ComplexNewLine props == Just CNL.Force
|| any (liftA2 (||) isSinglelineComment isComplexNode) nodes
&& lookupRule ComplexNewLine prefixProps /= Just CNL.None
&& lookupProperty ComplexNewLine props /= Just CNL.None

(colWidths, formattedCache, headerWasExtracted) =
maxColumnLengthsWithCache rs cursor nodes
Expand Down Expand Up @@ -397,7 +395,7 @@ doFormatNode rs cursor state elems =
. V.toList
$ elems

indentationAmount = fromMaybe 4 (lookupRule Indent prefixProps)
indentationAmount = fromMaybe 4 (lookupProperty Indent props)
in if complexChildren
then
T.unlines
Expand Down Expand Up @@ -453,8 +451,8 @@ formatWithCursor rs state cursor (ObjectKey (k, v)) =
in paddedKey <> " : " <> valueText
formatWithCursor _ _ _ (Comment comment) = formatComment comment
formatWithCursor rs _ cursor n =
let ps = findPropertiesForCursor PrefixMatch cursor rs
preserve = (Just True == lookupRule PreserveNumberFormat ps)
let ps = findPropertiesForCursor cursor rs
preserve = (Just True == lookupProperty PreserveNumberFormat ps)
in applyPadLogic (formatScalarNode preserve) ps n

formatNode :: RuleSet -> Node -> Text
Expand Down
137 changes: 67 additions & 70 deletions src/JbeamEdit/Formatting/Rules.hs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

module JbeamEdit.Formatting.Rules (
MatchMode (..),
NodePatternSelector (..),
NodePattern (..),
SomeKey (..),
SomeProperty (..),
PropertyKey (..),
Expand All @@ -18,29 +17,29 @@ module JbeamEdit.Formatting.Rules (
lookupKey,
allProperties,
deprecatedAliases,
prefixProperties,
keyName,
applyPadLogic,
complexNewLine,
lookupRule,
lookupProperty,
lookupPropertyForCursor,
findPropertiesForCursor,
) where

import Data.Bool (bool)
import Data.Foldable (fold)
import Data.Function (on)
import Data.List (find)
import Data.List (find, sortOn)
import Data.Map (Map)
import Data.Map qualified as M
import Data.Ord (Down (..))
import Data.Sequence (Seq (..))
import Data.Sequence qualified as Seq (length, null)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Type.Equality ((:~:) (Refl))
import JbeamEdit.Core.Node
import JbeamEdit.Core.NodeCursor qualified as NC
import JbeamEdit.Core.NodePath (NodeSelector (..))
import JbeamEdit.Core.NodePath qualified as NP (NodeSelector (..))
import JbeamEdit.Formatting.Rules.ComplexNewLine (ComplexNewLine)
import JbeamEdit.Formatting.Rules.ComplexNewLine qualified as CNL
import JbeamEdit.Formatting.Rules.TrailingComma (TrailingComma)
Expand All @@ -49,32 +48,21 @@ import Text.Read qualified as TR
data NodePatternSelector
= AnyObjectKey
| AnyArrayIndex
| Selector NodeSelector
deriving stock (Eq, Read, Show)

instance Ord NodePatternSelector where
compare a b = compare (rank a) (rank b)
where
rank :: NodePatternSelector -> (Int, Maybe NodeSelector)
rank AnyArrayIndex = (2, Nothing)
rank AnyObjectKey = (1, Nothing)
rank (Selector s) = (0, Just s)

newtype NodePattern
= NodePattern (Seq NodePatternSelector)
| Selector NP.NodeSelector
deriving stock (Eq, Read, Show)

instance Monoid RuleSet where
mempty = RuleSet M.empty
mempty = RuleSet M.empty [] mempty mempty M.empty M.empty

instance Semigroup RuleSet where
(RuleSet rs1) <> (RuleSet rs2) = RuleSet (M.unionWith M.union rs1 rs2)

instance Ord NodePattern where
compare (NodePattern a) (NodePattern b) =
case on compare (Down . Seq.length) a b of
EQ -> compare a b
c -> c
(RuleSet rs1 ps1 aok1 aai1 h1 b1) <> (RuleSet rs2 ps2 aok2 aai2 h2 b2) =
RuleSet
(M.unionWith (<>) rs1 rs2)
(mergePrefixes ps1 ps2)
(aok1 <> aok2)
(aai1 <> aai2)
(h1 <> h2)
(b1 <> b2)

data PropertyKey a where
AutoPad :: PropertyKey Bool
Expand Down Expand Up @@ -190,6 +178,18 @@ intProperties = map SomeKey [PadAmount, PadDecimals, Indent]
allProperties :: [SomeKey]
allProperties = boolProperties ++ enumProperties ++ intProperties

mergePrefixes :: [(Text, RuleSet)] -> [(Text, RuleSet)] -> [(Text, RuleSet)]
mergePrefixes ps1 ps2 =
sortOn (Down . T.length . fst) . M.toList . M.fromListWith (flip (<>)) $
ps1 <> ps2

prefixProperties :: [SomeKey]
prefixProperties =
SomeKey ComplexNewLine
: SomeKey TrailingComma
: SomeKey PreserveNumberFormat
: map SomeKey [PadAmount, PadDecimals, Indent]

-- | Maps deprecated property names to (key, value-when-true, value-when-false).
deprecatedAliases :: [(Text, (SomeKey, SomeProperty, SomeProperty))]
deprecatedAliases =
Expand All @@ -213,22 +213,26 @@ deprecatedAliases =

type Rule = Map SomeKey SomeProperty

newtype RuleSet
= RuleSet (Map NodePattern Rule)
data RuleSet
= RuleSet
{ rsBySelectors :: Map NP.NodeSelector RuleSet
, rsPrefixes :: [(Text, RuleSet)]
, rsAnyObjectKey :: Maybe RuleSet
, rsAnyArrayIndex :: Maybe RuleSet
, rsHere :: Rule
, rsBelow :: Rule
}
deriving stock (Eq, Read, Show)

lookupProp :: (Eq a, Read a, Show a) => PropertyKey a -> Rule -> Maybe a
lookupProp targetKey m =
lookupProperty :: (Eq a, Read a, Show a) => PropertyKey a -> Rule -> Maybe a
lookupProperty targetKey m =
case M.lookup (SomeKey targetKey) m of
Just (SomeProperty key val) ->
case eqKey key targetKey of
Just Refl -> Just val
Nothing -> Nothing
Nothing -> Nothing

lookupRule :: (Eq a, Read a, Show a) => PropertyKey a -> Rule -> Maybe a
lookupRule = lookupProp

applyDecimalPadding :: Int -> Text -> Text
applyDecimalPadding padDecimals node
| padDecimals /= 0
Expand All @@ -240,50 +244,43 @@ applyDecimalPadding padDecimals node

applyPadLogic :: (Node -> Text) -> Rule -> Node -> Text
applyPadLogic f rs n =
let padAmount = sum $ lookupProp PadAmount rs
padDecimals = sum $ lookupProp PadDecimals rs
let padAmount = sum $ lookupProperty PadAmount rs
padDecimals = sum $ lookupProperty PadDecimals rs
decimalPaddedText
| isNumberNode n = applyDecimalPadding padDecimals (f n)
| otherwise = f n
in bool (T.justifyLeft padAmount ' ' decimalPaddedText) (f n) (isComplexNode n)

complexNewLine :: RuleSet -> NC.NodeCursor -> Maybe ComplexNewLine
complexNewLine rs cursor =
let ps = findPropertiesForCursor PrefixMatch cursor rs
in lookupProp ComplexNewLine ps

data MatchMode = PrefixMatch | ExactMatch deriving (Eq, Show)
let ps = findPropertiesForCursor cursor rs
in lookupProperty ComplexNewLine ps

lookupPropertyForCursor
:: (Eq a, Read a, Show a)
=> MatchMode -> PropertyKey a -> RuleSet -> NC.NodeCursor -> Maybe a
lookupPropertyForCursor matchMode key rs cursor = lookupProp key (findPropertiesForCursor matchMode cursor rs)

comparePC :: NodePatternSelector -> NC.NodeBreadcrumb -> Bool
comparePC AnyObjectKey (NC.ObjectIndexAndKey _ _) = True
comparePC AnyArrayIndex (NC.ArrayIndex _) = True
comparePC (Selector s) bc = NC.compareSB s bc
comparePC _ _ = False

compareCursorAndPattern :: MatchMode -> NC.NodeCursor -> NodePattern -> Bool
compareCursorAndPattern matchMode (NC.NodeCursor c) (NodePattern p) = sameBy matchMode comparePC p c

type SelCrumbCompFun = NodePatternSelector -> NC.NodeBreadcrumb -> Bool

sameBy
:: MatchMode
-> SelCrumbCompFun
-> Seq NodePatternSelector
-> Seq NC.NodeBreadcrumb
-> Bool
sameBy matchMode f = go
=> PropertyKey a -> RuleSet -> NC.NodeCursor -> Maybe a
lookupPropertyForCursor key rs cursor =
lookupProperty key (findPropertiesForCursor cursor rs)

findPropertiesForCursor :: NC.NodeCursor -> RuleSet -> Rule
findPropertiesForCursor (NC.NodeCursor cursor) = go cursor
where
go (p :<| ps) (b :<| bs) =
let res = f p b
in res && go ps bs
go ps bs = Seq.null ps && (Seq.null bs || PrefixMatch == matchMode)

-- TODO: migrate to M.filterKeys once the stack snapshot ships containers 0.8
findPropertiesForCursor :: MatchMode -> NC.NodeCursor -> RuleSet -> Rule
findPropertiesForCursor matchMode cursor (RuleSet rs) =
fold (M.filterWithKey (const . compareCursorAndPattern matchMode cursor) rs)
go Empty rs = rs.rsHere <> rs.rsBelow
go (NC.ObjectIndexAndKey i k :<| bs) rs =
go
bs
( addBelowProps rs $
fold (M.lookup (NP.ObjectKey k) rs.rsBySelectors)
<> fold (M.lookup (NP.ObjectIndex i) rs.rsBySelectors)
<> matchingPrefixes k rs.rsPrefixes
<> fold rs.rsAnyObjectKey
)
go (NC.ArrayIndex i :<| bs) rs =
go
bs
( addBelowProps rs $
fold (M.lookup (NP.ArrayIndex i) rs.rsBySelectors)
<> fold rs.rsAnyArrayIndex
)
addBelowProps rsAbove rs = rs {rsBelow = rs.rsBelow <> rsAbove.rsBelow}
matchingPrefixes k = foldMap snd . filter ((`T.isPrefixOf` k) . fst)
Loading
Loading