@@ -29,11 +29,13 @@ import Protolude (
2929hush,
3030isJust,
3131optional,
32+putErrLn,
3233rightToMaybe,
3334show,
3435toStrict,
3536($),
3637(&),
38+(+),
3739(.),
3840(<$>),
3941(<&>),
@@ -70,7 +72,8 @@ import Data.Time.ISO8601.Duration qualified as Iso
7072import Data.ULID (ULID, ulidFromInteger)
7173import Data.ULID.TimeStamp (getULIDTimeStamp)
7274import Data.Vector qualified as V
73-import Data.Yaml as Yaml (ParseException, decodeEither')
75+import Data.Yaml (ParseException (InvalidYaml), YamlException (YamlException, YamlParseException), YamlMark (YamlMark))
76+import Data.Yaml qualified as Yaml
7477import Database.SQLite.Simple as Sql (Connection, query_)
7578import FullTask (FullTask)
7679import Lib (
@@ -723,18 +726,53 @@ data PreEdit
723726 | NoPreEdit
724727725728729+{-| Edit the task until it is valid YAML and can be decoded.
730+| Return the the tuple `(task, valid YAML content)`
731+-}
732+editUntilValidYaml
733+:: PreEdit
734+-> Connection
735+-> P.ByteString
736+-> P.ByteString
737+-> IO (Either ParseException (ImportTask, P.ByteString))
738+editUntilValidYaml preEdit conn initialYaml wipYaml = do
739+ yamlAfterEdit <- case preEdit of
740+ApplyPreEdit editFunc -> pure $ editFunc wipYaml
741+NoPreEdit -> runUserEditorDWIM yamlTemplate wipYaml
742+743+if yamlAfterEdit == initialYaml
744+then pure $ Left $ InvalidYaml $ Just $ YamlException "⚠️ Nothing changed"
745+else do
746+case yamlAfterEdit & Yaml.decodeEither' of
747+Left error -> do
748+case error of
749+-- Adjust the line and column numbers to be 1-based
750+InvalidYaml
751+ (Just (YamlParseException prblm ctxt (YamlMark idx line col))) ->
752+let yamlMark = YamlMark (idx + 1) (line + 1) (col + 1)
753+in putErrLn $
754+Yaml.prettyPrintParseException
755+ ( InvalidYaml
756+ (Just (YamlParseException prblm ctxt yamlMark))
757+ )
758+<> "\n"
759+ _ ->
760+ putErrLn $ Yaml.prettyPrintParseException error <> "\n"
761+ editUntilValidYaml preEdit conn initialYaml yamlAfterEdit
762+---
763+Right newTask -> do
764+pure $ Right (newTask, yamlAfterEdit)
765+766+726767editTaskByTask :: PreEdit -> Connection -> Task -> IO (Doc AnsiStyle)
727768editTaskByTask preEdit conn taskToEdit = do
728769 taskYaml <- taskToEditableYaml conn taskToEdit
729- newContent <- case preEdit of
730-ApplyPreEdit editFunc -> pure $ editFunc taskYaml
731-NoPreEdit -> runUserEditorDWIM yamlTemplate taskYaml
732-733-if newContent == taskYaml
734-then
735-pure $
736-"⚠️ Nothing changed" <+> hardline
737-else do
770+ taskYamlTupleRes <- editUntilValidYaml preEdit conn taskYaml taskYaml
771+case taskYamlTupleRes of
772+Left error -> pure $ pretty $ Yaml.prettyPrintParseException error
773+Right (importTaskRecord, newContent) -> do
774+ effectiveUserName <- getEffectiveUserName
775+ now <- getULIDTimeStamp <&> (show >>> T.toLower)
738776let
739777parseMetadata :: Value -> Parser Bool
740778 parseMetadata val = case val of
@@ -747,60 +785,51 @@ editTaskByTask preEdit conn taskToEdit = do
747785748786 hasMetadata =
749787 parseMaybe parseMetadata
750-=<< (rightToMaybe $ Yaml.decodeEither' newContent :: Maybe Value)
788+=<< rightToMaybe (Yaml.decodeEither' newContent)
789+790+ taskFixed =
791+ importTaskRecord.task
792+ { Task.user =
793+if importTaskRecord.task.user == ""
794+then T.pack effectiveUserName
795+else importTaskRecord.task.user
796+ , Task.metadata =
797+if hasMetadata == Just True
798+then importTaskRecord.task.metadata
799+else Nothing
800+ , -- Set to previous value to force SQL trigger to update it
801+Task.modified_utc = taskToEdit.modified_utc
802+ }
803+ notesCorrectUtc =
804+ importTaskRecord.notes
805+<&> ( \note ->
806+ note
807+ { Note.ulid =
808+if zeroUlidTxt `T.isPrefixOf` note.ulid
809+then note.ulid & T.replace zeroUlidTxt now
810+else note.ulid
811+ }
812+ )
751813752-decodeResult :: Either ParseException ImportTask
753- decodeResult = Yaml.decodeEither' newContent
814+ updateTask conn taskFixed
754815755-case decodeResult of
756-Left error -> die $ show error <> " in task \n" <> show newContent
757-Right importTaskRecord -> do
758- effectiveUserName <- getEffectiveUserName
759- now <- getULIDTimeStamp <&> (show >>> T.toLower)
760-let
761- taskFixed =
762- importTaskRecord.task
763- { Task.user =
764-if importTaskRecord.task.user == ""
765-then T.pack effectiveUserName
766-else importTaskRecord.task.user
767- , Task.metadata =
768-if hasMetadata == Just True
769-then importTaskRecord.task.metadata
770-else Nothing
771- , -- Set to previous value to force SQL trigger to update it
772-Task.modified_utc = taskToEdit.modified_utc
773- }
774- notesCorrectUtc =
775- importTaskRecord.notes
776-<&> ( \note ->
777- note
778- { Note.ulid =
779-if zeroUlidTxt `T.isPrefixOf` note.ulid
780-then note.ulid & T.replace zeroUlidTxt now
781-else note.ulid
782- }
783- )
784-785- updateTask conn taskFixed
786-787--- TODO: Remove after it was added to `createSetClosedUtcTrigger`
788--- Update again with the same `state` field to avoid firing
789--- SQL trigger which would overwrite the `closed_utc` field.
790-P.when (isJust taskFixed.closed_utc) $ do
791- now_ <- dateCurrent
792- updateTask conn taskFixed{Task.modified_utc = show @DateTime now_}
793-794- tagWarnings <- insertTags conn Nothing taskFixed importTaskRecord.tags
795- noteWarnings <- insertNotes conn Nothing taskFixed notesCorrectUtc
796-pure $
797- tagWarnings
798-<$$> noteWarnings
799-<$$> "✏️ Edited task"
800-<+> dquotes (pretty taskFixed.body)
801-<+> "with ulid"
802-<+> dquotes (pretty taskFixed.ulid)
803-<+> hardline
816+-- TODO: Remove after it was added to `createSetClosedUtcTrigger`
817+-- Update again with the same `state` field to avoid firing
818+-- SQL trigger which would overwrite the `closed_utc` field.
819+P.when (isJust taskFixed.closed_utc) $ do
820+ now_ <- dateCurrent
821+ updateTask conn taskFixed{Task.modified_utc = show @DateTime now_}
822+823+ tagWarnings <- insertTags conn Nothing taskFixed importTaskRecord.tags
824+ noteWarnings <- insertNotes conn Nothing taskFixed notesCorrectUtc
825+pure $
826+ tagWarnings
827+<$$> noteWarnings
828+<$$> "✏️ Edited task"
829+<+> dquotes (pretty taskFixed.body)
830+<+> "with ulid"
831+<+> dquotes (pretty taskFixed.ulid)
832+<+> hardline
804833805834806835editTask :: Config -> Connection -> IdText -> IO (Doc AnsiStyle)