Skip to content

Commit 96b1c0e

Browse files
committed
Support for language annotations like /* bash*/
that should remain as block comments when directly preceding string literals, while other block comments get converted to line comments. - Detect language annotations: single-line, non-doc comments with valid language identifiers - Preserve as `/* lang */` block comment syntax instead of converting to `# lang` line comments - Works with both regular strings `"..."` and indented strings `''...''`
1 parent 4540342 commit 96b1c0e

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

src/Nixfmt/Lexer.hs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
module Nixfmt.Lexer (lexeme, pushTrivia, takeTrivia, whole) where
77

88
import Control.Monad.State.Strict (MonadState, evalStateT, get, modify, put)
9-
import Data.Char (isSpace)
9+
import Data.Char (isAlphaNum, isSpace)
10+
import Data.Functor (($>))
1011
import Data.List (dropWhileEnd)
1112
import Data.Maybe (fromMaybe)
1213
import Data.Text as Text (
1314
Text,
15+
all,
1416
isPrefixOf,
1517
length,
1618
lines,
@@ -43,6 +45,7 @@ import Text.Megaparsec (
4345
chunk,
4446
getSourcePos,
4547
hidden,
48+
lookAhead,
4649
many,
4750
manyTill,
4851
notFollowedBy,
@@ -59,6 +62,8 @@ data ParseTrivium
5962
PTLineComment Text Pos
6063
| -- Track whether it is a doc comment
6164
PTBlockComment Bool [Text]
65+
| -- | Language annotation like /* lua */ (single line, non-doc)
66+
PTLanguageAnnotation Text
6267
deriving (Show)
6368

6469
preLexeme :: Parser a -> Parser a
@@ -127,6 +132,30 @@ blockComment = try $ preLexeme $ do
127132
commonIndentationLength :: Int -> [Text] -> Int
128133
commonIndentationLength = foldr (min . Text.length . Text.takeWhile (== ' '))
129134

135+
languageAnnotation :: Parser ParseTrivium
136+
languageAnnotation = try $ do
137+
-- Parse a block comment and extract its content
138+
PTBlockComment False [content] <- blockComment
139+
isStringDelimiterNext <- lookAhead isNextStringDelimiter
140+
141+
if isStringDelimiterNext && isValidLanguageIdentifier content
142+
then return (PTLanguageAnnotation (strip content))
143+
else fail "Not a language annotation"
144+
where
145+
-- Check if a text is a valid language identifier for language annotations
146+
isValidLanguageIdentifier txt =
147+
let stripped = strip txt
148+
in not (Text.null stripped)
149+
&& Text.length stripped <= 30 -- TODO: make configurable or remove limit
150+
&& Text.all (\c -> isAlphaNum c || c `elem` ['-', '+', '.', '_', '$', '{', '}']) stripped
151+
152+
-- Parser to peek at the next token to see if it's a string delimiter (" or '')
153+
isNextStringDelimiter = do
154+
_ <- manyP isSpace -- Skip any remaining whitespace
155+
(chunk "\"" $> True)
156+
<|> (chunk "''" $> True)
157+
<|> pure False
158+
130159
-- This should be called with zero or one elements, as per `span isTrailing`
131160
convertTrailing :: [ParseTrivium] -> Maybe TrailingComment
132161
convertTrailing = toMaybe . join . map toText
@@ -148,6 +177,7 @@ convertLeading =
148177
PTBlockComment _ [] -> []
149178
PTBlockComment False [c] -> [LineComment $ " " <> strip c]
150179
PTBlockComment isDoc cs -> [BlockComment isDoc cs]
180+
PTLanguageAnnotation c -> [LanguageAnnotation c]
151181
)
152182

153183
isTrailing :: ParseTrivium -> Bool
@@ -169,7 +199,7 @@ convertTrivia pts nextCol =
169199
_ -> (convertTrailing trailing, convertLeading leading)
170200

171201
trivia :: Parser [ParseTrivium]
172-
trivia = many $ hidden $ lineComment <|> blockComment <|> newlines
202+
trivia = many $ hidden $ languageAnnotation <|> lineComment <|> blockComment <|> newlines
173203

174204
-- The following primitives to interact with the state monad that stores trivia
175205
-- are designed to prevent trivia from being dropped or duplicated by accident.

src/Nixfmt/Pretty.hs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ instance Pretty TrailingComment where
8686
instance Pretty Trivium where
8787
pretty EmptyLine = emptyline
8888
pretty (LineComment c) = comment ("#" <> c) <> hardline
89+
pretty (LanguageAnnotation lang) = comment ("/* " <> lang <> " */") <> hardspace
8990
pretty (BlockComment isDoc c) =
9091
comment (if isDoc then "/**" else "/*")
9192
<> hardline
@@ -105,10 +106,23 @@ instance (Pretty a) => Pretty (Item a) where
105106

106107
-- For lists, attribute sets and let bindings
107108
prettyItems :: (Pretty a) => Items a -> Doc
108-
prettyItems (Items items) = sepBy hardline items
109+
prettyItems (Items items) = go items
110+
where
111+
go [] = mempty
112+
go [item] = pretty item
113+
-- Special case: language annotation comment followed by string item
114+
go (Comments [LanguageAnnotation lang] : Item stringItem : rest) =
115+
pretty (LanguageAnnotation lang)
116+
<> hardspace
117+
<> group stringItem
118+
<> if null rest then mempty else hardline <> go rest
119+
go (item : rest) =
120+
pretty item <> if null rest then mempty else hardline <> go rest
109121

110122
instance Pretty [Trivium] where
111123
pretty [] = mempty
124+
-- Special case: if trivia consists only of a single language annotation, render it inline without a preceding hardline
125+
pretty [langAnnotation@(LanguageAnnotation _)] = pretty langAnnotation
112126
pretty trivia = hardline <> hcat trivia
113127

114128
instance (Pretty a) => Pretty (Ann a) where

src/Nixfmt/Types.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ data Trivium
7272
| -- Multi-line comments with /* or /**. Multiple # comments are treated as a list of `LineComment`.
7373
-- The bool indicates a doc comment (/**)
7474
BlockComment Bool [Text]
75+
| -- | Language annotation comments like /* lua */ that should remain as block comments before strings
76+
LanguageAnnotation Text
7577
deriving (Eq, Show)
7678

7779
type Trivia = [Trivium]

0 commit comments

Comments
 (0)