Summary
docs find-replace --format markdown (and the markdown surgical-edit path) does not preserve the surrounding paragraph structure of the matched range. When the match sits inside a bulleted/numbered/nested list item, a heading, or a styled run, the replacement comes out as un-bulleted NORMAL_TEXT with run styling stripped. The matched item falls out of its list / loses its heading level / loses bold-italic-underline-link at the seam.
This is a delete-then-insert that throws away the deleted range's bullet/listId, namedStyleType, paragraph indent, and textRun.textStyle, then re-inserts freshly-parsed markdown that only carries whatever structure the replacement text itself declares.
Root cause (single shared helper)
replacePreparedDocsMarkdownRange in internal/cmd/docs_mutation.go builds, in one batchUpdate:
DeleteContentRange over the matched range,
InsertText of the freshly-parsed markdown at the start index,
resetDocsTextStyleRequest(...) — an UpdateTextStyle with an empty TextStyle{} and Fields = "bold,italic,underline,strikethrough,smallCaps,baselineOffset,foregroundColor,backgroundColor,fontSize,weightedFontFamily,link" over the inserted range (clears any run styling the insertion inherited at the seam), and
- when the replacement is not a single inline paragraph,
resetDocsParagraphRequests(...) — a DeleteParagraphBullets plus an UpdateParagraphStyle forcing NamedStyleType: NORMAL_TEXT and zero indentStart/indentFirstLine.
So the surrounding bullet, list membership, heading level, indent, and seam run-styling are all actively reset. The structure that comes out is only whatever MarkdownToDocsRequests (internal/docsmarkdown/requests.go) re-emits from the replacement string — it has no knowledge of the deleted range's prior paragraph.
Relevant source:
internal/cmd/docs_mutation.go — replacePreparedDocsMarkdownRange (the DeleteContentRange + InsertText, then resetDocsTextStyleRequest at line ~176 and resetDocsParagraphRequests at line ~182).
internal/cmd/docs_mutation.go — resetDocsParagraphRequests (DeleteParagraphBullets + UpdateParagraphStyle{NamedStyleType: NORMAL_TEXT, indentStart:0, indentFirstLine:0}).
internal/cmd/docs_text_style.go — resetDocsTextStyleRequest (empty TextStyle{} with the wide Fields reset list).
internal/docsmarkdown/requests.go — MarkdownToDocsRequests only emits namedStyleType/CreateParagraphBullets/text styles that the replacement markdown declares.
Command wiring: docs find-replace routes every --format markdown replacement (and --first --format markdown) through runMarkdown -> replaceDocsMarkdownRange -> replacePreparedDocsMarkdownRange (internal/cmd/docs_edit.go, DocsFindReplaceCmd.Run / runMarkdown).
Minimal reproducer
Create a scratch doc with one bulleted list item, then markdown-replace its text:
DOC=$(gog docs create "gog-repro-bullet" --json | jq -r .documentId)
# Seed a bulleted list: "Item one" / "Item two"
printf -- '- Item one\n- Item two\n' > /tmp/seed.md
gog docs write "$DOC" --replace --markdown --file /tmp/seed.md
# Confirm both paragraphs carry paragraph.bullet { listId, ... }
gog docs raw "$DOC" | jq '.body.content[].paragraph | select(.elements) | {text: .elements[0].textRun.content, bullet: .bullet}'
# Replace the text of the first item with markdown
gog docs find-replace "$DOC" "Item one" "Item one edited" --first --format markdown
# Re-read: the edited paragraph now has NO .bullet and namedStyleType NORMAL_TEXT
gog docs raw "$DOC" | jq '.body.content[].paragraph | select(.elements) | {text: .elements[0].textRun.content, bullet: .bullet, style: .paragraphStyle.namedStyleType}'
Heading variant — seed # Heading A, then find-replace "Heading A" "Heading A edited" --first --format markdown; the paragraph's namedStyleType flips from HEADING_1 to NORMAL_TEXT.
Styled-run variant — seed a paragraph containing a bold word, match that word with --format markdown and a plain replacement; the replacement run loses textStyle.bold (the seam reset clears it).
Expected vs actual
- Expected: replacing the text of a list item keeps the paragraph in its list (
bullet.listId preserved); replacing a heading's text keeps its namedStyleType; replacing part of a styled run keeps the surrounding run's styling unless the replacement markdown overrides it.
- Actual: the paragraph loses its bullet/list membership and is forced to
NORMAL_TEXT with zero indent; headings drop to NORMAL_TEXT; styled runs are reset to default at the seam.
Notes / workaround
--format plain without --first routes through the native ReplaceAllText API (runDocsReplaceAll), which is an in-place text swap and preserves bullets, named style, and run styling. So the current safe path for editing list/heading/styled text is plain find-replace (whole-document or with a unique match), accepting that it cannot introduce new markdown formatting. The markdown path needs to read the matched paragraph's existing bullet/namedStyleType/paragraphStyle and re-apply them (or skip the resets) when the replacement does not itself declare paragraph structure.
Reproducer commands above were derived from the latest main source (internal/cmd/docs_mutation.go et al. at commit 21ca030d); I was unable to run them live in my environment (binary execution sandboxed), so please confirm the JSON before/after on a scratch doc.
Summary
docs find-replace --format markdown(and the markdown surgical-edit path) does not preserve the surrounding paragraph structure of the matched range. When the match sits inside a bulleted/numbered/nested list item, a heading, or a styled run, the replacement comes out as un-bulleted NORMAL_TEXT with run styling stripped. The matched item falls out of its list / loses its heading level / loses bold-italic-underline-link at the seam.This is a delete-then-insert that throws away the deleted range's
bullet/listId,namedStyleType, paragraph indent, andtextRun.textStyle, then re-inserts freshly-parsed markdown that only carries whatever structure the replacement text itself declares.Root cause (single shared helper)
replacePreparedDocsMarkdownRangeininternal/cmd/docs_mutation.gobuilds, in one batchUpdate:DeleteContentRangeover the matched range,InsertTextof the freshly-parsed markdown at the start index,resetDocsTextStyleRequest(...)— anUpdateTextStylewith an emptyTextStyle{}andFields = "bold,italic,underline,strikethrough,smallCaps,baselineOffset,foregroundColor,backgroundColor,fontSize,weightedFontFamily,link"over the inserted range (clears any run styling the insertion inherited at the seam), andresetDocsParagraphRequests(...)— aDeleteParagraphBulletsplus anUpdateParagraphStyleforcingNamedStyleType: NORMAL_TEXTand zeroindentStart/indentFirstLine.So the surrounding bullet, list membership, heading level, indent, and seam run-styling are all actively reset. The structure that comes out is only whatever
MarkdownToDocsRequests(internal/docsmarkdown/requests.go) re-emits from the replacement string — it has no knowledge of the deleted range's prior paragraph.Relevant source:
internal/cmd/docs_mutation.go—replacePreparedDocsMarkdownRange(theDeleteContentRange+InsertText, thenresetDocsTextStyleRequestat line ~176 andresetDocsParagraphRequestsat line ~182).internal/cmd/docs_mutation.go—resetDocsParagraphRequests(DeleteParagraphBullets+UpdateParagraphStyle{NamedStyleType: NORMAL_TEXT, indentStart:0, indentFirstLine:0}).internal/cmd/docs_text_style.go—resetDocsTextStyleRequest(emptyTextStyle{}with the wideFieldsreset list).internal/docsmarkdown/requests.go—MarkdownToDocsRequestsonly emitsnamedStyleType/CreateParagraphBullets/text styles that the replacement markdown declares.Command wiring:
docs find-replaceroutes every--format markdownreplacement (and--first --format markdown) throughrunMarkdown -> replaceDocsMarkdownRange -> replacePreparedDocsMarkdownRange(internal/cmd/docs_edit.go,DocsFindReplaceCmd.Run/runMarkdown).Minimal reproducer
Create a scratch doc with one bulleted list item, then markdown-replace its text:
Heading variant — seed
# Heading A, thenfind-replace "Heading A" "Heading A edited" --first --format markdown; the paragraph'snamedStyleTypeflips fromHEADING_1toNORMAL_TEXT.Styled-run variant — seed a paragraph containing a bold word, match that word with
--format markdownand a plain replacement; the replacement run losestextStyle.bold(the seam reset clears it).Expected vs actual
bullet.listIdpreserved); replacing a heading's text keeps itsnamedStyleType; replacing part of a styled run keeps the surrounding run's styling unless the replacement markdown overrides it.NORMAL_TEXTwith zero indent; headings drop toNORMAL_TEXT; styled runs are reset to default at the seam.Notes / workaround
--format plainwithout--firstroutes through the nativeReplaceAllTextAPI (runDocsReplaceAll), which is an in-place text swap and preserves bullets, named style, and run styling. So the current safe path for editing list/heading/styled text is plainfind-replace(whole-document or with a unique match), accepting that it cannot introduce new markdown formatting. The markdown path needs to read the matched paragraph's existingbullet/namedStyleType/paragraphStyleand re-apply them (or skip the resets) when the replacement does not itself declare paragraph structure.Reproducer commands above were derived from the latest
mainsource (internal/cmd/docs_mutation.goet al. at commit21ca030d); I was unable to run them live in my environment (binary execution sandboxed), so please confirm the JSON before/after on a scratch doc.