Skip to content

feat(docs): add find-range primitive that maps text to Document index ranges #682

Description

@sebsnyk

Motivation

The Docs API addresses every mutation by character index, but documents.get returns text spread across textRuns. Today, any script that wants to "do X to the literal text Y" must (1) call gog docs raw, (2) walk body.content concatenating textRuns, (3) compute start/end indices itself. Every consumer reinvents this walk, and gets the whitespace / HTML-entity edge cases wrong on the way.

Exposing a first-class find-range helper unblocks every other "by-text" mode (insert / delete / update / format etc.) and is also the foundation for resolving comment anchors.

Consumers

This is a load-bearing primitive. Direct consumers across the open enhancement set:

Repro

Today, to delete a literal phrase a script has to do this:

gog docs raw <DOC_ID> -j \
  | jq -r '.body.content[] | .paragraph?.elements[]? | select(.textRun) | "\(.startIndex)\t\(.endIndex)\t\(.textRun.content)"' \
  | awk -F'\t' '\$3 ~ /\(PDF output also supported\)/' \
  | ... manual offset math ...
gog docs delete <DOC_ID> --start <START> --end <END>

Proposed surface

gog docs find-range <docId> <text> [flags]
  --match-case            Case-sensitive (default off)
  --all                   Return every occurrence (default: first only)
  --occurrence INT        Return the Nth occurrence (1-based)
  --normalize-whitespace  Collapse runs of whitespace in both haystack and needle (default on)
  --tab=STRING            Target a specific tab by title or ID

JSON output:

{
  "matches": [
    {"startIndex": 1234, "endIndex": 1267, "paragraphIndex": 12, "tabId": "kix.abc"}
  ]
}

Plain output: TSV startIndex<TAB>endIndex<TAB>paragraphIndex<TAB>tabId.

Behaviour notes:

  • Normalises whitespace (collapses tabs / newlines / repeated spaces) before searching, but returns indices on the unnormalised body.
  • HTML-entity-decodes the needle if it looks pre-encoded (&quot;, &amp;, &#39;, &#183;, &gt;, &lt;).
  • Returns exit code 3 (matching --fail-empty convention) when no matches.

Acceptance criteria

  • Single-occurrence and multi-occurrence (--all) modes both return correct indices verified against documents.get.
  • Matches span across textRun boundaries inside the same paragraph (Docs splits runs on style boundaries; finder must not assume one run = one paragraph).
  • Whitespace-normalised matches still report correct unnormalised indices.
  • Works inside tab content with --tab= resolution.
  • Returns empty matches array (not an error) when text not found, unless --fail-empty is set.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions