module Error.Diagnose
  ( -- $header

    -- * How to use this module
    -- $usage

    -- ** Generating a report
    -- $generate_report

    -- ** Creating diagnostics from reports
    -- $create_diagnostic

    -- *** Pretty-printing a diagnostic
    -- $diagnostic_pretty

    -- *** Exporting a diagnostic to JSON
    -- $diagnostic_json

    -- ** Compatibility layers for popular parsing libraries
    -- $compatibility_layers

    -- *** megaparsec >= 9.0.0 ("Error.Diagnose.Compat.Megaparsec")
    -- $compatibility_megaparsec
     
    -- *** parsec >= 3.1.14.0 ("Error.Diagnose.Compat.Parsec")
    -- $compatibility_parsec
    
    -- *** Common errors
    -- $compatibility_errors

    -- * Re-exports
    module Export ) where

import Error.Diagnose.Pretty as Export
import Error.Diagnose.Position as Export
import Error.Diagnose.Report as Export
import Error.Diagnose.Diagnostic as Export

{- $header

   This module exports all the needed data types to use this library.
   It should be sufficient to only @import "Error.Diagnose"@.
-}

{- $usage

   This library is intended to provide a very simple way of creating beautiful errors, by exposing
   a small yet simple API to the user.

   The basic idea is that a diagnostic is a collection of reports (which embody errors or warnings) along
   with the files which can be referenced in those reports.
-}

{- $generate_report

   A report contains:

   - A message, to be shown at the top

   - A list of located markers, used to underline parts of the source code and to emphasize it with a message

   - A list of hints, shown at the very bottom

   __Note__: The message type contained in a report is abstracted by a type variable.
             In order to render the report, the message must also be able to be rendered in some way
             (that we'll see later).

   This library allows defining two kinds of reports:

   - Errors, using 'err'

   - Warnings, using 'warn'

   Both take a message, a list of located markers and a list of hints.

   A very simple example is:

   > exampleReport :: Report String
   > exampleReport =
   >   err
   >     -- vv  ERROR MESSAGE
   >     "This is my first error report"
   >     -- vv  MARKERS
   >     [ (Position (1, 3) (1, 8) "some_test.txt", This "Some text under the marker") ]
   >     -- vv  HINTS
   >     []

   In general, 'Position's are returned by either a lexer or a parser, so that you never have to construct them
   directly in the code.

   __Note__: If using any parser library, you will have to convert from the internal positioning system to a 'Position'
             to be able to use this library.

   Markers put in the report can be one of (the colors specified are used only when pretty-printing):

   - A 'This' marker, which is the primary marker of the report.
     While it is allowed to have multiple of these inside one report, it is encouraged not to, because the position at the top of
     the report will only be the one of the /first/ 'This' marker, and because the resulting report may be harder to understand.

         This marker is output in red in an error report, and yellow in a warning report.

   - A 'Where' marker contains additional information/provides context to the error/warning report.
     For example, it may underline where a given variable @x@ is bound to emphasize it.

         This marker is output in blue.

   - A 'Error.Diagnose.Report.Maybe' marker may contain possible fixes (if the text is short, else hints are recommended for this use).

         This marker is output in magenta.
-}

{- $create_diagnostic

   To create a new diagnostic, you need to use its 'Data.Default.Default' instance (which exposes a 'def' function, returning a new empty 'Diagnostic').
   Once the 'Diagnostic' is created, you can use either 'addReport' (which takes a 'Diagnostic' and a 'Report', abstract by the same message type,
   and returns a 'Diagnostic') to insert a new report inside the diagnostic, or 'addFile' (which takes a 'Diagnostic', a 'FilePath' and a @['String']@,
   and returns a 'Diagnostic') to insert a new file reference in the diagnostic.

   You can then either pretty-print the diagnostic obtained (which requires all messages to be instances of the 'Text.PrettyPrint.ANSI.Leijen.Pretty')
   or export it to a lazy JSON 'Data.Bytestring.Lazy.ByteString' (e.g. in a LSP context).
-}

{- $diagnostic_pretty

   'Diagnostic's can be output to any 'System.IO.Handle' using the 'printDiagnostic' function.
   This function takes several parameters:

   - The 'System.IO.Handle' onto which to output the 'Diagnostic'.
     It __must__ be a 'System.IO.Handle' capable of outputting data.

   - A 'Bool' used to indicate whether you want to output the 'Diagnostic' with unicode characters, or simple ASCII characters.

         Here are two examples of the same diagnostic, the first output with unicode characters, and the second output with ASCII characters:

         > [error]: Error with one marker in bounds
         >      ╭─▶ test.zc@1:25-1:30
         >      │
         >    1 │ let id<a>(x : a) : a := x + 1
         >      •                         ┬────
         >      •                         ╰╸ Required here
         > ─────╯

         > [error]: Error with one marker in bounds
         >      +-> test.zc@1:25-1:30
         >      |
         >    1 | let id<a>(x : a) : a := x + 1
         >      :                         ^----
         >      :                         `- Required here
         > -----+

   - A 'Bool' set to 'False' if you don't want colors in the end result.

   - And finally the 'Diagnostic' to output.
-}

{- $diagnostic_json

   'Diagnostic's can be exported to a JSON record of the following type, using the 'diagnosticToJson' function:

   > { files:
   >     { name: string
   >     , content: string[]
   >     }[]
   > , reports:
   >     { kind: 'error' | 'warning'
   >     , message: string
   >     , markers:
   >         { kind: 'this' | 'where' | 'maybe'
   >         , position:
   >             { beginning: { line: int, column: int }
   >             , end: { line: int, column: int }
   >             , file: string
   >             }
   >         , message: string
   >         }[]
   >     , hints: string[]
   >     }[]
   > }

   This is particularly useful in the context of a LSP server, where outputting or parsing a raw error yields strange results or is unnecessarily complicated.

   Please note that this requires the flag @diagnose:json@ to be enabled (it is disabled by default in order not to include @aeson@, which is a heavy library).
-}

{- $compatibility_layers 

   There are many parsing libraries available in the Haskell ecosystem, each coming with its own way of handling errors.
   Eventually, one needs to be able to map errors from these libraries to 'Diagnostic's, without having to include additional code for doing so.
   This is where compatibility layers come in handy.

   As of now, there are compatibility layers for these libraries:
-}

{- $compatibility_megaparsec

   This needs the flag @diagnose:megaparsec-compat@ to be enabled.

   Using the compatibility layer is very easy, as it is designed to be as simple as possible.
   One simply needs to convert the 'Text.Megaparsec.ParseErrorBundle' which is returned by running a parser into a 'Diagnostic' by using 'Error.Diagnose.Compat.Megaparsec.diagnosticFromBundle'.
   Several wrappers are included for easy creation of kinds (error, warning) of diagnostics.

   __Note:__ the returned diagnostic does not include file contents, which needs to be added manually afterwards.

   As a quick example:

   > import qualified Text.Megaparsec as MP
   > import qualified Text.Megaparsec.Char as MP
   > import qualified Text.Megaparsec.Char.Lexer as MP
   >
   > let filename = "<interactive>"
   >     content  = "00000a2223266"
   >
   > let myParser = MP.some MP.decimal <* MP.eof
   >
   > let res      = MP.runParser myParser filename content
   >
   > case res of
   >   Left bundle ->
   >     let diag  = errorDiagnosticFromBundle "Parse error on input" Nothing bundle
   >            --   Creates a new diagnostic with no default hints from the bundle returned by megaparsec
   >         diag' = addFile diag filename content
   >            --   Add the file used when parsing with the same filename given to 'MP.runParser'
   >     in printDiagnostic stderr True True diag'
   >   Right res   -> print res

   This example will return the following error message (assuming default instances for @'Error.Diagnose.Compat.Megaparsec.HasHints' 'Data.Void.Void' msg@):

   > [error]: Parse error on input
   >      ╭─▶ <interactive>@1:6-1:7
   >      │
   >    1 │ 00000a2223266
   >      •      ┬ 
   >      •      ├╸ unexpected 'a'
   >      •      ╰╸ expecting digit, end of input, or integer
   > ─────╯
-}

{- $compatibility_parsec

   This needs the flag @diagnose:parsec-compat@ to be enabled.

   This compatibility layer allows easily converting 'Text.Parsec.Error.ParseError's into a single-report diagnostic containing all available information such
   as unexpected/expected tokens or error messages.
   The function 'Error.Diagnose.Compat.Parsec.diagnosticFromParseError' is used to perform the conversion between a 'Text.Parsec.Error.ParseError' and a 'Diagnostic'.

   __Note:__ the returned diagnostic does not include file contents, which needs to be added manually afterwards.

   Quick example:

   > import qualified Text.Parsec as P
   >
   > let filename = "<interactive>"
   >     content  = "00000a2223266"
   >
   > let myParser = P.many1 P.digit <* P.eof
   >
   > let res      = P.parse myParser filename content
   >
   > case res of
   >   Left error ->
   >     let diag  = errorDiagnosticFromParseError "Parse error on input" Nothing error
   >            --   Creates a new diagnostic with no default hints from the bundle returned by megaparsec
   >         diag' = addFile diag filename content
   >            --   Add the file used when parsing with the same filename given to 'MP.runParser'
   >     in printDiagnostic stderr True True diag'
   >   Right res  -> print res

   This will output the following errr on @stderr@:

   > [error]: Parse error on input
   >      ╭─▶ <interactive>@1:6-1:7
   >      │
   >    1 │ 00000a2223266
   >      •      ┬ 
   >      •      ├╸ unexpected 'a'
   >      •      ╰╸ expecting any of digit, end of input
   > ─────╯
-}

{- $compatibility_errors

   - @No instance for (HasHints ??? msg) arising from a use of ‘errorDiagnosticFromBundle’@ (@???@ is any type, depending on your parser's custom error type):

       The typeclass 'Error.Diagnose.Compat.Megaparsec.HasHints' does not have any default instances, because treatments of custom errors is highly dependent on who is using the library.
       As such, you will need to create orphan instances for your parser's error type.

       Note that the message type @msg@ can be left abstract if the implements of 'Error.Diagnose.Compat.Hints.hints' is @hints _ = mempty@.

-}