Skip to content

docs find-replace --format markdown drops bullets / heading style / run styling of the matched paragraph (delete-then-insert loses surrounding structure) #838

Description

@sebsnyk

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:

  1. DeleteContentRange over the matched range,
  2. InsertText of the freshly-parsed markdown at the start index,
  3. 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
  4. 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.goreplacePreparedDocsMarkdownRange (the DeleteContentRange + InsertText, then resetDocsTextStyleRequest at line ~176 and resetDocsParagraphRequests at line ~182).
  • internal/cmd/docs_mutation.goresetDocsParagraphRequests (DeleteParagraphBullets + UpdateParagraphStyle{NamedStyleType: NORMAL_TEXT, indentStart:0, indentFirstLine:0}).
  • internal/cmd/docs_text_style.goresetDocsTextStyleRequest (empty TextStyle{} with the wide Fields reset list).
  • internal/docsmarkdown/requests.goMarkdownToDocsRequests 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions