Skip to content

Move or copy a comment

A comment follows its owner through every edit (see Comment ownership). When the owner is going away, or the note describes something else after a reorganisation, ChangeComments gives it a new owner on purpose, and ReplaceWithComments does the same while it replaces a node.

err := f.Edit(func(tx *yamldoc.Transaction) error {
    // notes: comments read from the current working snapshot
    // owner: the CommentOwner they should belong to
    return tx.ChangeComments(yamldoc.CommentChanges{
        Moves: []yamldoc.CommentMove{{Sources: notes, To: yamldoc.AtOwner(owner, yamldoc.Leading())}},
    })
})

Moving a note before its owner goes

# Connection settings
server:
  # Managed by deployment automation
  retries: 3

Replacing server with a scalar removes retries and its note with it. Move the note to server first, in the same transaction, and it survives:

# Connection settings
# Managed by deployment automation
server: disabled

The complete program is the ChangeComments example. Both commands commit together or not at all.

Selecting comments and owners

Read the comments from the snapshot you are editing: Entry.Comments, Node.ItemComments, Node.Comments, Document.Comments and Snapshot.Comments. Each returns the comments that owner holds, in source order. Two comments with the same text are two occurrences, and the handle you pass is the one that moves.

The destination is a CommentOwner: Entry.CommentOwner, Node.ItemCommentOwner(i), or Node.CommentOwner for a mapping or sequence itself. Like a Node, every handle belongs to one working revision, so take tx.Snapshot() again before each command. A handle from an earlier revision is ErrStale.

Where a comment can go

A comment can go to a mapping entry or sequence item, to the tail of a block mapping or sequence, or beside a document marker (below), in the same document or another document of the same file. A source can be any comment, including one at the end of a line, a file header, or a note between documents.

Placement Owner Lands
Leading() entry, item After the owner's existing leading comments, directly above its key or dash
BeforeComment(c) entry, item Directly above c, a standalone comment the same owner holds
AfterComment(c) entry, item Directly below c
Inline() entry, item At the end of the owner's line: after a scalar value, after a block scalar's header, or after the key or dash of a block collection
Tail() block mapping or sequence After its last member and any comments already at its tail
server: # Inline() on the server entry
  port: 8080 # Inline() on the port entry
  # Tail() on the server mapping

Comments take the owner's indentation and the file's line endings. A comment moved to the end of a line whose own comment moves away in the same command keeps that comment's spacing.

Documents and the stream

Five destinations sit beside document markers rather than at an owner, and each needs its marker to be in the file already: a comment command never adds a --- or ....

Destination Owner Lands
AtStartMarker(doc) the document At the end of its --- line; one comment
AtEndMarker(doc) the document At the end of its ... line; one comment
InDocumentGap(before, after) the stream Between before's ... and after's directives or ---, after any notes already there
InStreamHeader(snapshot) the stream Before the first document's directives or ---, after the header already there
InStreamFooter(snapshot) the stream At the end of the file, after the last document's ... and the footer already there
# InStreamHeader
--- # AtStartMarker(first)
retries: 3
... # AtEndMarker(first)
# InDocumentGap(first, second)
---
log_level: info
...
# InStreamFooter
``` A group
lands in the order it had in the file, whatever order you pass the handles
in. A blank line that separated the comments does not travel with them.

## Moving and copying

A `CommentMove` takes the selected comments away from where they are. A
`CommentCopy` leaves them and writes new ones at every destination it names.
One command can do both:

```go
yamldoc.CommentChanges{
    Moves:  []yamldoc.CommentMove{{Sources: notes, To: yamldoc.AtOwner(relocated, yamldoc.Leading())}},
    Copies: []yamldoc.CommentCopy{{Sources: notes, To: []yamldoc.Destination{yamldoc.AtOwner(replica, yamldoc.Leading())}}},
}

Passing the same handle twice selects it once, and the same destination twice in one copy writes it once. Two copy commands write two copies. An empty selection succeeds and changes nothing, so a filter that matched no comments needs no special case.

What is refused

Nothing changes when a command is refused, and the transaction cannot commit afterwards.

Request Error
A copy with no destination, a move with none, or one comment moved to two places ErrCommand
Two groups for the same place in one command ErrPlacement: their order would be a guess, so send two commands in the order you want
A move anchored on a comment that the same command moves ErrPlacement
An anchor another owner holds, or one at the end of a line ErrPlacement
An entry that shares its line with something else, such as the first key after a sequence dash or a key inside a one-line flow mapping ErrPlacement: a comment on that line would belong to another owner
Inline() for more than one comment, or on a line that already ends in a comment that is staying ErrPlacement: a line has one end
Tail() on a flow collection, or on an entry or item ErrPlacement
A container's tail and the next entry's documentation in one command ErrPlacement: they are the same place in the file, so send two commands
AtOwner with a document or the stream as the owner ErrPlacement: use one of the destinations beside a marker
A marker, gap, header or footer whose --- or ... is not in the file ErrPlacement
AtStartMarker or AtEndMarker for more than one comment, or on a marker line that already holds a comment that is staying, or a value ErrPlacement
InDocumentGap for documents that are not neighbours in that order ErrPlacement

Replacing a node and keeping its comments

A replacement drops the comments its old value owned, and a scalar with a comment on its line keeps that comment by turning into a flow collection. ReplaceWithComments replaces a node and moves or copies comments in one command, and InReplacement names an owner inside the value being written:

to := yamldoc.InReplacement([]yamldoc.Step{yamldoc.StringStep("retries")}, yamldoc.CommentOnMappingEntry, yamldoc.Inline())

err := tx.ReplaceWithComments(root, map[string]any{"retries": 5}, yamldoc.CommentChanges{
    Moves: []yamldoc.CommentMove{{Sources: notes, To: to}},
})
5 # Retry budget

becomes

retries: 5 # Retry budget

The path runs from the value being written. With CommentOnMappingEntry or CommentOnSequenceItem its last step names the entry or item; with CommentOnContainer the path names the mapping or sequence itself, and an empty path is the value. Any placement but BeforeComment and AfterComment works, since the new value holds no comments to anchor on. Destinations outside the replacement work as in ChangeComments.

Moved comments leave before the value is written, so they no longer hold it to the flow form, and every step is checked before anything commits. A path the value does not have, a role it does not fit, or an owner the replacement removes is ErrPlacement; InReplacement anywhere but ReplaceWithComments is ErrCommand. The complete program is the ReplaceWithComments example.