#+TITLE: org-gdocs
#+OPTIONS: toc:2

Publish a projection of an org-mode document to Google Docs and sync reviewer
comments back into the org file.

Org-mode is the *single source of truth*; Google Docs is a *collaboration surface
for comments only*. You author in Emacs; your organization reviews and comments in
Docs; their comments flow back into the org file as ~TODO~ headings. Document
content never flows the other way.

* What it does

| Command | Effect |
|---------+--------|
| ~org-gdocs auth~ | Run the Google OAuth loopback flow and cache a token. |
| ~org-gdocs push <file>~ | Project the org body into the linked Doc (creating it on first push). |
| ~org-gdocs pull <file>~ | Fetch reviewer comments into the file's ~** Active Comments~ subtree. |
| ~org-gdocs clean <file>~ | Remove comment subtrees you have marked ~DONE~. |
| ~org-gdocs open <file>~ | Print the linked Doc's browser URL. |

The binary is *stateless across runs*: everything it needs lives in the org file
itself (plus the cached OAuth token). It reads and writes the file directly and
prints a single s-expression result envelope for the Emacs shell to consume.

* Hard limitations (read these first)

These are consequences of how Google's APIs work, not choices we can revisit:

- *Comments are read-only and you create them in the Docs UI.* The Drive API
  cannot create a properly *anchored* comment — comments created via the API
  render as un-anchored. So the tool *never creates comments*; it only reads them
  (~comments.list~) and resolves them (~comments.update {resolved:true}~). Anchor a
  comment by selecting text in the Docs UI like any reviewer.
- *Comments anchor to a section, not a character range.* The Docs comment anchor
  blob is undocumented and brittle, so a pulled comment is filed under the
  *containing heading* (by ~:CUSTOM_ID:~), or document-level — never a precise
  offset. When the anchor is unusable we fall back to matching the comment's quoted
  text against the projected section text.
- *First tab only.* Google Docs "tabs" (2024+) change indexing; the tool operates
  on the first/legacy tab. A document with multiple tabs is reported as
  unsupported rather than silently mis-indexed. Docs the tool creates are
  single-tab.
- *Every push is a full replace.* When the content changes, push clears the Doc body
  and re-inserts the whole projection; it never diffs. Edits made directly in the Doc
  are discarded on the next push (org is the source of truth). A full replace deletes
  the text reviewer comments anchor to, so **changing content orphans existing
  comments** ("original content deleted") — pull comments into the org file before you
  re-push content. As an optimization, a push whose projection is *unchanged* since
  the last push (e.g. you only posted a reply or resolved a comment) **skips the body
  write entirely**, so anchored comments survive an unchanged re-push.

* The two-region file model

A synced file splits at the ~* GDOC_METADATA :noexport:~ heading:

- *Body* (everything above the line): your prose, preserved *byte-for-byte*. The
  tool parses it read-only to build the projection and never regenerates it. The
  one and only body mutation is inserting a ~:CUSTOM_ID:~ property under a heading
  that lacks one — pure metadata that changes neither structure nor prose.
- *Header keywords* — ~#+GDOC_ID:~, ~#+GDOC_URL:~, ~#+GDOC_LAST_PUSH:~,
  ~#+GDOC_LAST_PULL:~ — and the *~* GDOC_METADATA~ subtree* are tool-owned:
  - ~** Sync State~ — one ~emacs-lisp~ source block holding a single s-expression:
    the position map (custom-id → index + element kind) and a collaborator cache.
    Pure machine state, regenerated each sync; never hand-edit it.
  - ~** Active Comments~ — human-facing ~TODO~ headings, one per comment, keyed by
    ~:COMMENT_ID:~. *Merge-by-id*: pull inserts new comments; existing headings are
    never rewritten, so your notes and clocking survive. Mark one ~DONE~ and the
    next push resolves it in Google; ~clean~ cuts the ~DONE~ subtrees.

* Supported org elements

Headings, paragraphs with inline *bold* / /italic/ / +strikethrough+ / ~inline
code~ / =verbatim= / links, ordered and unordered lists (including checkboxes),
tables, ~src~ and ~example~ blocks (as monospace), block quotes, and horizontal
rules. Constructs without a Docs equivalent degrade to visible literal text.

Known v1 degradations: table cells are plain text (in-cell inline formatting is
dropped), a horizontal rule renders as a run of ~─~ glyphs, and soft line breaks
become hard paragraph breaks. Image projection is *deferred* (out of scope for v1).

* Setup

1. *Create OAuth credentials.* In the Google Cloud console, enable the Google Docs
   and Google Drive APIs, create an *OAuth client ID of type "Desktop app"*, and
   download the client-secret JSON.
2. *Place the credentials.* Point the config at the downloaded file. Config lives
   at ~$XDG_CONFIG_HOME/org-gdocs/config.toml~ (TOML); the credential and token
   files are JSON by Google's requirement (the only JSON surfaces).
3. *Authenticate.* Run ~org-gdocs auth~ (or ~C-c g~ nothing — call it once from a
   shell). The loopback flow opens a browser; approve the two least-privilege
   scopes (~documents~ + ~drive.file~). The token is cached and refreshed
   automatically.

* Emacs integration

Load ~elisp/org-gdocs.el~ and enable ~org-gdocs-mode~ in an org buffer:

#+begin_src emacs-lisp
(add-to-list 'load-path "/path/to/crates/org-gdocs/elisp")
(require 'org-gdocs)
(setq org-gdocs-program "org-gdocs") ; or an absolute path
#+end_src

The command prefix is ~org-gdocs-prefix-key~ (default ~C-c g~). Rebind it with
~setopt~ / ~:custom~ if it collides with another package — e.g. gptel also binds
~C-c g~, so you might use ~C-c d~:

#+begin_src emacs-lisp
(setopt org-gdocs-prefix-key "C-c d")
#+end_src

| Key | Command |
|-----+---------|
| ~C-c g s~ | dwim — publish, resolve, or fetch by buffer state |
| ~C-c g p~ | push |
| ~C-c g f~ | pull (fetch comments) |
| ~C-c g c~ | clean (archive ~DONE~) |
| ~C-c g o~ | open the Doc in a browser |
| ~C-c g j~ | jump between a comment and its section (either direction) |
| ~C-c g r~ | reply — insert a reply skeleton under the comment at point |
| ~C-c g k~ | close — mark the comment at point ~DONE~ (push resolves it) |
| ~C-c g n~ / ~C-c g N~ | next / previous comment |
| ~C-c g d~ | toggle client-side debug output |

The elisp is a *thin shell*: it saves the buffer, runs the binary, reads the
s-expression result, and reverts the buffer (point is preserved because the body
is unchanged on disk). It contains no projection, anchoring, or Google logic.

** Replying to and closing comments

Comments are read-only and you anchor them in the Docs UI (D1), but you *can*
**reply** to one: under the comment's ~TODO~ heading, add a ~REPLY~ subheading and
type your answer (~C-c g r~ inserts the skeleton):

#+begin_src org
,*** TODO Alice: please clarify this sentence
:PROPERTIES:
:COMMENT_ID: AAAB...
:END:
,#+begin_quote
the quoted sentence
,#+end_quote
,**** REPLY
Clarified in the next revision.
#+end_src

The next ~push~ posts each unposted ~REPLY~ to Google via the Drive replies API
(replies, unlike anchored comments, *can* be created), then records it in ~** Sync
State~ so it is never posted twice. (Editing a reply's text after it posts makes it
a new reply.)

To **close** a comment, mark its heading ~DONE~ (~C-c g k~); the next ~push~
resolves it in Google, and ~clean~ later removes the archived subtree.

* Typical workflow

1. ~C-c g p~ to push the file; a Google Doc is created and linked.
2. Share the Doc; reviewers comment on it in the Docs UI.
3. ~C-c g f~ to pull their comments in as ~TODO~ headings.
4. Address each; mark it ~DONE~ when handled.
5. ~C-c g p~ pushes the latest content and resolves the ~DONE~ comments in Google.
6. ~C-c g c~ archives the resolved comment subtrees from the file.
