ai-agent-diff-patch-0.0.1.0: src/AIAgent/DiffPatch/Unified/ProjectedContext/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
-- | ProjectedContext layer: unified diff parser.
-- Contains the Megaparsec-based parser for unified diff format.
-- All functions are pure; no IO is performed in this module.
module AIAgent.DiffPatch.Unified.ProjectedContext.Parser
( Parser
, parsePatch
, pPatch
, pFileHeader
, pHunk
, pHunkHeader
, pDiffLine
, pRestOfLine
) where
import qualified Data.Text as T
import Data.Text (Text)
import AIAgent.DiffPatch.Unified.CoreModel.Type
-- megaparsec imports
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
type Parser = Parsec Void Text
-- | Parse a unified diff Text into a Patch.
-- Accepts both the headed format ("--- ..." / "+++ ..." lines present) and
-- the bare format (starting directly with "@@ ... @@").
-- Returns InvalidPatch on parse failure.
parsePatch :: Text -> Either PatchError Patch
parsePatch input =
case runParser pPatch "" input of
Left err -> Left (InvalidPatch (T.pack (errorBundlePretty err)))
Right p -> Right p
-- | Top-level parser: optional file header, then one or more hunks.
pPatch :: Parser Patch
pPatch = do
mHeader <- optional pFileHeader
hunks <- some pHunk
eof
return (Patch mHeader hunks)
-- | Parse the "--- old\n+++ new\n" file header.
pFileHeader :: Parser FileHeader
pFileHeader = do
_ <- string "--- "
oldPath <- pRestOfLine
_ <- string "+++ "
newPath <- pRestOfLine
return (FileHeader oldPath newPath)
-- | Parse a single hunk: header line followed by diff lines.
pHunk :: Parser Hunk
pHunk = do
(os, oc, ns, nc) <- pHunkHeader
hlines <- many pDiffLine
return (Hunk os oc ns nc hlines)
-- | Parse "@@ -oldStart[,oldCount] +newStart[,newCount] @@[trailing]\n".
-- The count field is optional; when absent it defaults to 1.
pHunkHeader :: Parser (Int, Int, Int, Int)
pHunkHeader = do
_ <- string "@@ -"
os <- L.decimal
oc <- option 1 (char ',' *> L.decimal)
_ <- string " +"
ns <- L.decimal
nc <- option 1 (char ',' *> L.decimal)
_ <- string " @@"
_ <- pRestOfLine -- consume optional trailing comment and the newline
return (os, oc, ns, nc)
-- | Parse a single diff line: context (' '), removed ('-'), or added ('+').
-- As an extension for LLM-generated patch tolerance, a bare newline ('\n') with
-- no leading space is also accepted and treated as an empty context line.
-- The three standard prefixes are tried first; the bare-newline branch is a fallback.
pDiffLine :: Parser Line
pDiffLine =
(char ' ' *> (Line Context <$> pRestOfLine))
<|> (char '-' *> (Line Removed <$> pRestOfLine))
<|> (char '+' *> (Line Added <$> pRestOfLine))
<|> (char '\n' *> return (Line Context ""))
-- ^ Empty context line (LLM-generated patch tolerance).
-- The Unified Diff spec requires " \n" but LLMs often emit bare "\n".
-- Placed last so the three standard prefixes are tried first.
-- | Consume the rest of the current line (not including the newline) and
-- then consume the newline itself. Returns the line content without '\n'.
pRestOfLine :: Parser Text
pRestOfLine = do
content <- T.pack <$> manyTill anySingle (char '\n')
return content