gdocs manual
Table of contents
Overview
The gdocs package provides bidirectional synchronization between
org-mode files and Google Docs. It lets you open, edit, create, and
push Google Docs entirely from within Emacs, using org-mode as the
native editing format. The package handles OAuth 2.0 authentication,
format conversion, incremental diffing, and conflict resolution so that
your org files and their Google Docs counterparts stay in sync.
The package addresses three main workflows:
Publish org to Google Docs. Create a Google Doc from an existing org file to share with collaborators. The two files remain in sync: edits to either propagate to the other.
Read and edit shared Google Docs. Open a Google Doc shared by someone else as an org-mode buffer in Emacs. Edit without leaving Emacs.
Import existing Google Docs. Import old Google Docs into org files. The files remain synchronized, but you may later choose to unlink or delete the Google Doc.
Additionally, the package supports directory-level linking: you can associate a local directory with a Google Drive folder so that newly created documents are automatically placed in the corresponding Drive folder, mirroring your local directory structure (Directory linking).
All commands are accessible through a transient popup menu
(Transient menu).
Architecture
The package is composed of several modules, each responsible for a distinct layer of functionality:
gdocs.el- Entry point, user commands, and the
gdocs-modeminor mode (Minor mode). gdocs-auth.el- OAuth 2.0 flow, token management, and multi-account support (Authentication).
gdocs-api.el- Asynchronous Google Docs and Google Drive API
client built on
plz.el(API client). gdocs-convert.el- Bidirectional conversion between org-mode and Google Docs JSON via an intermediate representation (IR) (Format conversion).
gdocs-diff.el- Incremental diff engine that computes the minimal set of API requests to synchronize changes, using word-level diffs to preserve Google Docs comment anchors (Diff engine).
gdocs-sync.el- Push/pull orchestration, shadow copy management, and conflict detection (Sync engine).
gdocs-merge.el- Side-by-side conflict resolution interface (Conflict resolution).
All API operations are fully asynchronous via plz.el so that Emacs
never blocks during network requests.
Dependencies
The package requires Emacs 29.1 or later and depends on the following external packages:
plz(0.7+) for asynchronous HTTP requests.dash(2.19+) for list manipulation utilities.s(1.13+) for string manipulation utilities.org(9.6+), which is built into Emacs.
Quick start
Obtain OAuth credentials from Google Cloud Console (Obtaining credentials).
Configure your credentials in Emacs:
(setopt gdocs-accounts '(("personal" . ((client-id . "YOUR-CLIENT-ID") (client-secret . "YOUR-CLIENT-SECRET")))))Authenticate:
M-x gdocs-authenticate.Open an existing Google Doc:
M-x gdocs-open.Or create a new Google Doc from an org file:
M-x gdocs-create.
Once a buffer is linked, the gdocs-mode minor mode activates
automatically. Changes are pushed to Google Docs on every save (when
gdocs-auto-push-on-save is non-nil), and you can pull remote changes
at any time with M-x gdocs-pull.
The development repository is on GitHub.
User options
Account configuration
The user option gdocs-accounts is an alist of named Google accounts
with their OAuth credentials. Each entry takes the form
(NAME . ((client-id . "ID") (client-secret . "SECRET"))) where NAME
is a string identifying the account (e.g., "personal" or "work"),
and client-id and client-secret are OAuth 2.0 credentials that you
obtain from the Google Cloud Console (Obtaining credentials).
When gdocs-accounts is nil or has only one entry, commands that
require an account use it automatically without prompting. When
multiple accounts are configured, commands prompt with
completing-read to select one. You would configure multiple accounts
when you need to access Google Docs from different Google Workspace
organizations (for example, a personal account and a work account with
separate OAuth credentials).
The default value is nil. You must configure at least one account
before using any gdocs commands.
Obtaining credentials
To use gdocs, you need an OAuth 2.0 client ID and client secret
from the Google Cloud Console. These credentials identify your
application to Google’s servers and allow it to request access to your
Google Docs and Drive data on your behalf. They are not passwords —
they do not grant access by themselves. The actual authorization
happens when you run gdocs-authenticate and grant consent in your
browser.
Follow these steps to create your credentials:
Go to the Google Cloud Console and sign in with the Google account you want to use with
gdocs.Create a new project (or select an existing one):
- Click the project dropdown at the top of the page.
- Click New Project.
- Enter a name (e.g., “Emacs gdocs”) and click Create.
Enable the required APIs. Go to APIs & Services > Library and enable both of these:
- Google Docs API (search for “Google Docs API”).
- Google Drive API (search for “Google Drive API”).
Configure the OAuth consent screen. Go to APIs & Services > OAuth consent screen:
- Select External as the user type (unless you have a Google Workspace organization and want to restrict access to internal users).
- Fill in the required fields: app name (e.g., “Emacs gdocs”) and your email address for the support and developer contact fields.
- On the Scopes page, add these two scopes:
https://www.googleapis.com/auth/documentshttps://www.googleapis.com/auth/drive
- On the Test users page, add your own email address. While the app is in “Testing” status (the default), only listed test users can authorize it.
- Click Save and Continue through the remaining pages.
Create the OAuth credentials. Go to APIs & Services > Credentials:
- Click Create Credentials > OAuth client ID.
- Select Desktop app as the application type.
- Enter a name (e.g., “Emacs gdocs”) and click Create.
- Google will display your Client ID and Client secret. Copy both values.
Configure
gdocswith the credentials you just obtained:(setopt gdocs-accounts '(("personal" . ((client-id . "123456789-abcdef.apps.googleusercontent.com") (client-secret . "GOCSPX-abcdefghijklmnop")))))Replace the example values with your actual client ID and client secret. The name
"personal"is arbitrary; use whatever label makes sense to you.Run
M-x gdocs-authenticate. Your browser will open to a Google consent screen. Grant access, andgdocswill store the resulting tokens securely ingdocs-token-directory(Account configuration).
Note: while the app remains in “Testing” status, Google will show a warning screen during authorization (“Google hasn’t verified this app”). Click Continue to proceed. You do not need to publish the app for personal use — “Testing” status works indefinitely for the test users you listed in step 4.
The user option gdocs-token-directory specifies the directory where
OAuth token files are stored. Each account’s tokens are saved as a
JSON file named ACCOUNT-NAME.json with restrictive file permissions
(600) to protect the sensitive token data. Parent directories are
created automatically when needed.
The default value is the gdocs/tokens/ subdirectory of
user-emacs-directory. You would change this if you want to store
tokens in a location managed by a secrets tool or a different
directory structure.
Sync behavior
The user option gdocs-auto-push-on-save controls whether changes are
automatically pushed to Google Docs whenever you save a linked org
buffer. When non-nil, saving the buffer triggers an asynchronous push
of any changes detected since the last sync.
The default value is t. Set this to nil if you prefer to push
changes manually with gdocs-push (Pushing and pulling). Automatic
pushing is suppressed when the buffer is in a conflict or error state,
regardless of this setting.
The user option gdocs-auto-pull-on-open controls whether a linked org
buffer automatically pulls the latest remote content when opened. When
non-nil, opening a file that has gdocs-mode active triggers an
immediate pull to ensure you start editing the most recent version.
The default value is nil. Enable this if you frequently collaborate
on documents and want to always start with the latest remote state.
When disabled, the buffer shows whatever was last saved locally, and you
must run gdocs-pull manually to fetch updates.
The user option gdocs-preserve-comments controls whether the push
flow checks for Google Docs comments before applying changes. When
non-nil, the push fetches all comments via the Drive Comments API and
identifies destructive operations—element deletions and cross-type
modifications—that would destroy anchored comments. For each at-risk
operation, you are prompted with y-or-n-p showing the comment author
and content. Declined deletions are removed from the request batch, so
the commented elements remain in the remote document.
The default value is t. Set this to nil if you never use Google
Docs comments and want to skip the extra API call on each push. This
option works with both manual pushes and auto-push-on-save
(Comment-aware push).
File storage
The user option gdocs-directory specifies the default directory where
newly imported org files are saved. When you use gdocs-open to fetch
a Google Doc for the first time, the resulting org file is written to
this directory with a filename derived from the document title.
The default value is ~/org/gdocs/. The directory is created
automatically if it does not exist. If you already have an org file
and use gdocs-create or gdocs-link, the file stays at its current
location regardless of this setting.
Org tagging
The user option gdocs-org-tag specifies an org tag to add to the
first heading of linked org files. When non-nil, every org file that
has a Google Doc counterpart receives this tag on its first heading,
making it easy to filter linked files in agenda views or tag-based
searches.
The value is a string (e.g., "gdocs") or nil to disable tagging.
The default value is "gdocs". The tag is added automatically when
you link, open, or create a document, and removed when you unlink
(Linking and unlinking).
Cross-document link resolution
The user option gdocs-link-directories specifies additional
directories to scan when resolving cross-document links during
push. When you push an org file that contains file: or id: links
pointing to other org files with Google Doc counterparts, the
converter replaces those links with Google Docs URLs so they work in
the browser. gdocs-directory (File storage) is always scanned;
this option lets you add further directories that also contain linked
org files.
The value is a list of directory paths. The default value is nil,
meaning only gdocs-directory is scanned. You would add directories
here if your linked org files are spread across multiple locations
(for example, a project-specific directory alongside your main gdocs
directory).
Commands
Opening and creating documents
The command gdocs-open fetches a Google Doc and opens it as an
org-mode buffer. It prompts for a document ID or a full Google Docs
URL (e.g., https://docs.google.com/document/d/DOCUMENT-ID/edit). If
multiple accounts are configured in gdocs-accounts
(Account configuration), it prompts you to select one. The document is
converted from Google Docs JSON to org-mode format via the intermediate
representation (Format conversion), saved to gdocs-directory
(File storage) with a sanitized filename, and displayed in a buffer
with gdocs-mode active.
The command gdocs-create does the reverse: it creates a new Google
Doc from the current org buffer’s content. The title defaults to the
buffer’s #+TITLE keyword, falling back to the buffer name. If the
current directory is linked to a Google Drive folder via
gdocs-folder-id (Directory linking), the new document is
automatically placed in that folder. After creating the document, the
buffer is linked to it and gdocs-mode is activated. Subsequent saves
push changes to the newly created Doc.
Both commands are autoloaded, so they are available without explicitly loading the package.
Pushing and pulling
The command gdocs-push pushes local changes to the linked Google Doc.
If a shadow copy exists (i.e., the buffer has been synced at least once
in the current session), the push is incremental: only the differences
between the current buffer and the shadow are sent as Google Docs API
batchUpdate requests (Diff engine). If no shadow exists (for
example, after restarting Emacs), the push performs a full document
replacement.
When gdocs-auto-push-on-save is non-nil, gdocs-push is called
automatically after every save. Pushes are serialized: if a push is
already in progress when another is triggered, the second push is
queued and executed after the first completes.
The command gdocs-pull fetches the latest version of the linked
Google Doc. It first checks the document’s revision ID via the Google
Drive API; if the revision matches the locally stored one, it reports
“Already up to date.” If the revision differs, it fetches the full
document, converts it to org-mode format, and either replaces the
buffer content silently (when there are no local modifications) or
triggers the conflict resolution interface (Conflict resolution).
Linking and unlinking
The command gdocs-link links the current org buffer to an existing
Google Doc. It prompts for a document ID or URL and, if needed, an
account. Linking writes the document ID and account to the file-level
property drawer (Metadata storage) and triggers an initial pull to
establish the sync baseline.
The command gdocs-unlink removes the gdocs properties from the
property drawer and clears all buffer-local sync state, turning the
org file back into a standalone document. The file content is
preserved; only the sync metadata is removed.
Directory linking
When you have a local directory of org files that you want to mirror in
a Google Drive folder, you can link the directory so that gdocs-create
automatically places new documents in the correct Google Drive folder
and uses the associated account.
The command gdocs-link-directory links the current directory to a
Google Drive folder. It prompts for a folder ID or a full Google Drive
folder URL (e.g., https://drive.google.com/drive/folders/FOLDER-ID)
and, if needed, an account. The mapping is written to .dir-locals.el
in the current directory as gdocs-folder-id and gdocs-account
entries. After linking, any call to gdocs-create
(Opening and creating documents) from within that directory
automatically places the new document in the linked folder.
The command gdocs-unlink-directory removes the gdocs-folder-id and
gdocs-account entries from the current directory’s .dir-locals.el.
The local files are not affected; only the Drive folder association is
removed.
The command gdocs-create-folder creates a Google Drive folder
corresponding to a local directory. It prompts for the target
directory, defaulting to default-directory or the directory at point
in dired-mode. The target must be inside a directory that is already
linked to a Google Drive folder (Directory linking). If there are
unlinked intermediate directories between the target and its nearest
linked ancestor, the command prompts before creating the intermediate
Drive folders as well, so the folder hierarchy on Drive matches the
local directory structure.
All three commands are autoloaded.
Status and browser
The command gdocs-status displays the current sync status in the echo
area. It shows the linked document ID, account name, sync status
(synced, modified, pushing, conflict, or error), revision ID, and the
timestamp of the last sync. This is useful for quickly verifying
whether the buffer is in sync without inspecting the modeline.
The command gdocs-open-in-browser opens the linked Google Doc in your
default web browser using browse-url. It constructs the URL from
the stored document ID. This is convenient when you need to check
formatting, share the link with someone, or use Google Docs features
that are not yet supported in gdocs. In a dired buffer, the
command operates on the file or directory at point: for files, it opens
the linked Google Doc; for directories, it opens the linked Google
Drive folder (Directory linking).
Authentication
The command gdocs-authenticate initiates the OAuth 2.0 flow for a
Google account (OAuth flow). If multiple accounts are configured, it
prompts you to select one. The command opens your browser to Google’s
consent screen, and a temporary local HTTP server receives the
authorization callback. Upon successful authentication, tokens are
stored in gdocs-token-directory (Account configuration).
The command gdocs-logout deletes the stored token file for an
account, effectively logging you out. You will need to run
gdocs-authenticate again to re-authorize.
Both commands are autoloaded.
Conflict resolution
When a pull detects that both local and remote changes exist, the
conflict resolution interface opens in a dedicated *gdocs-merge*
buffer using gdocs-merge-mode (Merge mode). The following commands
are available in the merge buffer:
gdocs-merge-next-hunk(n) moves to the next unresolved conflict hunk.gdocs-merge-previous-hunk(p) moves to the previous unresolved hunk.gdocs-merge-accept-local(a) accepts the local version of the current hunk.gdocs-merge-accept-remote(b) accepts the remote version of the current hunk.gdocs-merge-edit-hunk(e) opens a temporary org-mode buffer pre-filled with the local text so you can edit the merged result manually. PressC-c C-cin the edit buffer to accept.gdocs-merge-accept-all-local(A) resolves all unresolved hunks with the local version.gdocs-merge-accept-all-remote(B) resolves all unresolved hunks with the remote version.gdocs-merge-finalize(C-c C-c) finalizes the merge once all hunks are resolved, applies the merged content to the original buffer, and triggers a push. It signals an error if any hunks remain unresolved.gdocs-merge-abort(C-c C-korq) aborts the merge after confirmation, leaving the buffer unchanged.
Transient menu
The command gdocs-menu opens a transient popup menu that provides
quick access to all gdocs commands and options from a single
interface. It is autoloaded, so you can invoke it with M-x gdocs-menu
without loading the package first.
The menu is organized into the following groups:
- Sync:
gdocs-push(p),gdocs-pull(l),gdocs-status(s). - Documents:
gdocs-open(o),gdocs-create(c),gdocs-open-in-browser(b). - Linking:
gdocs-link(L),gdocs-unlink(U),gdocs-link-directory(D),gdocs-unlink-directory(X),gdocs-create-folder(F). - Auth:
gdocs-authenticate(a),gdocs-logout(x). - Options: toggle
gdocs-auto-push-on-save(-p), togglegdocs-auto-pull-on-open(-l), setgdocs-directory(-d), setgdocs-org-tag(-t), setgdocs-link-directories(-L).
The Options group uses transient infix commands that let you view and
modify user options directly from the menu without visiting your init
file. Changes made through the menu take effect immediately for the
current session but are not persisted to disk; use setopt in your
configuration to persist them.
Functions
Authentication
The function gdocs-auth-select-account selects a Google account from
gdocs-accounts. If only one account is configured, it returns that
account’s name without prompting. If multiple accounts exist, it
prompts via completing-read with an optional PROMPT argument. It
signals an error if gdocs-accounts is nil. This function is used
internally by most commands but is also available for use in custom
workflows or hooks.
The function gdocs-auth-get-access-token ensures a valid OAuth access
token for a given account, then calls a callback with the token string.
If the token is expired or about to expire (within a 60-second safety
margin), it refreshes the token asynchronously before calling the
callback. If no token exists, it initiates the full OAuth flow. This
function is the primary entry point for all API operations that need
authentication.
API client
Google Docs API
The function gdocs-api-get-document fetches a Google Docs document by
its ID. It calls CALLBACK with the parsed JSON document structure.
This is used internally by gdocs-pull to retrieve the remote document
state.
The function gdocs-api-batch-update sends a list of batchUpdate
requests to a document, which is how all modifications (insertions,
deletions, style changes) are applied to Google Docs. The REQUESTS
argument is a list of request alists as specified by the Google Docs
API. This function is called by both the incremental diff engine and
full replacement paths.
The function gdocs-api-create-document creates a new empty Google
Docs document with a given title. The callback receives the response
JSON, which includes the new documentId.
All three functions accept an optional ACCOUNT argument; when nil,
the default account is selected automatically
(Account configuration).
Google Drive API
The function gdocs-api-get-file-metadata fetches metadata for a
Google Drive file, including its id, name, modifiedTime,
version, and headRevisionId. The revision ID is used by the pull
mechanism to detect whether the remote document has changed
(Pushing and pulling).
The function gdocs-api-list-files lists Google Drive files matching a
query string (using Drive’s query syntax). It supports pagination via
an optional PAGE-TOKEN argument.
The function gdocs-api-upload-image uploads a local image file to
Google Drive using a multipart upload. It determines the MIME type
from the file extension (PNG, JPEG, GIF, WebP, and SVG are supported).
An optional FOLDER-ID argument specifies the parent folder in Drive.
The function gdocs-api-search-files performs a full-text search across
Google Drive by wrapping gdocs-api-list-files with a
fullText contains query.
The function gdocs-api-list-comments fetches all comments on a Google
Drive file via the Drive Comments API. It paginates automatically with
a page size of 100, collecting results across pages before calling
CALLBACK with the complete list of comment alists. Each comment
includes its id, content, quotedFileContent (the anchored text),
author, createdTime, resolved state, and nested replies. This
function is used by the comment-aware push flow when
gdocs-preserve-comments is non-nil (Comment-aware push).
The function gdocs-api-resolve-comment resolves a comment on a Google
Drive file via the Drive Comments API. It takes a FILE-ID and
COMMENT-ID and marks the comment as resolved by sending a PATCH request
that sets the resolved field to true. CALLBACK receives the updated
comment alist. This function is used by the comment-aware push flow
when the user chooses the “resolve & delete” action
(Comment-aware push).
The function gdocs-api-rename-file renames a Google Drive file
identified by FILE-ID to NEW-NAME. This is used internally by
gdocs-push when the local #+TITLE has changed since the last sync,
so the Google Doc title stays in sync with the org file
(Pushing and pulling).
The function gdocs-api-create-folder creates a new folder in Google
Drive. An optional PARENT-FOLDER-ID argument places the folder inside
a specific parent. This function is called by gdocs-create-folder
when building the Drive folder hierarchy for local directories
(Directory linking).
The function gdocs-api-move-file moves a Google Drive file into a
different folder by adding the file to the target folder and removing
it from its current parent. This is used by gdocs-create to place
newly created documents in the linked Drive folder
(Directory linking).
Format conversion
All conversion passes through a flat list of intermediate representation (IR) element plists, which serves as the canonical interchange format shared with the diff engine (Diff engine).
The function gdocs-convert-org-buffer-to-ir parses the current org
buffer using org-element-parse-buffer and returns an IR element list.
The related function gdocs-convert-org-string-to-ir does the same but
accepts an org-mode string rather than operating on the current buffer.
The function gdocs-convert-org-buffer-to-segments is a variant of
gdocs-convert-org-buffer-to-ir designed for the three-way merge.
Instead of returning a flat IR list, it returns a plist with four keys:
:ir (the full IR element list), :segments (a list of segment plists,
each pairing an :ir-element with its :org-text region), :preamble
(buffer text before the first body element, including the property
drawer and #+TITLE: line), and :postamble (text after the last
element). By preserving the mapping between IR
elements and their original org text, the three-way merge can retain
org-only metadata (property drawers, TODO keywords, tags) that would
otherwise be lost during conversion (Sync engine).
The function gdocs-convert-ir-to-org converts an IR element list back
to a well-formatted org-mode string. This is the inverse of
gdocs-convert-org-buffer-to-ir and is used when pulling remote
changes.
The function gdocs-convert-docs-json-to-ir converts a Google Docs API
JSON response (an alist with keys like body, title, lists, and
namedRanges) to an IR element list. It walks the document structure
and maps each structural element to its IR equivalent. Each resulting
IR element carries :doc-start and :doc-end properties copied from
the JSON’s startIndex and endIndex, giving the diff engine
authoritative UTF-16 document positions
(Intermediate representation).
The function gdocs-convert-ir-to-docs-requests converts an IR element
list to a list of Google Docs batchUpdate request alists. It tracks
a running UTF-16 insertion index starting at 1 (the beginning of the
Google Docs body). This function is used for full document replacement
and by the diff engine for incremental insertions.
Diff engine
The function gdocs-diff-generate is the main entry point of the diff
engine. It compares two IR element lists (OLD-IR and NEW-IR) and
returns a list of batchUpdate request plists representing the minimal
changes needed. Internally, it computes content-based keys for each
element, runs a longest-common-subsequence (LCS) algorithm to identify
kept, deleted, inserted, and modified elements, and generates the
appropriate API requests.
An optional START-INDEX argument specifies the UTF-16 code unit offset
where the first IR element begins in the document body (default 1).
The sync engine uses this to account for title elements that precede the
diffable body content (Sync engine). When the old IR carries
:doc-start / :doc-end properties (as produced by
gdocs-convert-docs-json-to-ir), the engine uses those authoritative
positions; otherwise it computes positions from element text lengths.
Requests are ordered for correct index stability: deletions and
modifications processed from highest index to lowest, with insertions
interleaved so that earlier operations do not shift indices of later
ones.
For workflows that need to inspect or filter diff operations before
generating API requests, two lower-level functions are available. The
function gdocs-diff-compute-operations returns the raw diff operation
list (with :op values of keep, delete, insert, or modify)
without generating requests. The function
gdocs-diff-generate-from-ops accepts a pre-computed operation list and
produces the corresponding API requests. The comment-aware push flow
uses this two-step API to filter declined deletions before generating
the final request batch (Comment-aware push).
Conflict resolution
The function gdocs-merge-start opens the conflict resolution
interface. It takes two org-mode strings (local and remote) and a
callback function. It computes hunks by running an LCS diff on the
line-level differences, sets up the *gdocs-merge* buffer with
color-coded overlays for conflicts, and displays it. When the user
finalizes the merge, the callback is called with the resulting merged
org string.
This function is called internally by the sync engine when a pull detects both local and remote changes (Conflict resolution).
Authentication
The command gdocs-authenticate initiates the OAuth 2.0 flow for a
Google account (OAuth flow). If multiple accounts are configured, it
prompts you to select one. The command opens your browser to Google’s
consent screen, and a temporary local HTTP server receives the
authorization callback. Upon successful authentication, tokens are
stored in gdocs-token-directory (Account configuration).
The command gdocs-logout deletes the stored token file for an
account, effectively logging you out. You will need to run
gdocs-authenticate again to re-authorize.
Both commands are autoloaded.
API client
Google Docs API
The function gdocs-api-get-document fetches a Google Docs document by
its ID. It calls CALLBACK with the parsed JSON document structure.
This is used internally by gdocs-pull to retrieve the remote document
state.
The function gdocs-api-batch-update sends a list of batchUpdate
requests to a document, which is how all modifications (insertions,
deletions, style changes) are applied to Google Docs. The REQUESTS
argument is a list of request alists as specified by the Google Docs
API. This function is called by both the incremental diff engine and
full replacement paths.
The function gdocs-api-create-document creates a new empty Google
Docs document with a given title. The callback receives the response
JSON, which includes the new documentId.
All three functions accept an optional ACCOUNT argument; when nil,
the default account is selected automatically
(Account configuration).
Google Drive API
The function gdocs-api-get-file-metadata fetches metadata for a
Google Drive file, including its id, name, modifiedTime,
version, and headRevisionId. The revision ID is used by the pull
mechanism to detect whether the remote document has changed
(Pushing and pulling).
The function gdocs-api-list-files lists Google Drive files matching a
query string (using Drive’s query syntax). It supports pagination via
an optional PAGE-TOKEN argument.
The function gdocs-api-upload-image uploads a local image file to
Google Drive using a multipart upload. It determines the MIME type
from the file extension (PNG, JPEG, GIF, WebP, and SVG are supported).
An optional FOLDER-ID argument specifies the parent folder in Drive.
The function gdocs-api-search-files performs a full-text search across
Google Drive by wrapping gdocs-api-list-files with a
fullText contains query.
The function gdocs-api-list-comments fetches all comments on a Google
Drive file via the Drive Comments API. It paginates automatically with
a page size of 100, collecting results across pages before calling
CALLBACK with the complete list of comment alists. Each comment
includes its id, content, quotedFileContent (the anchored text),
author, createdTime, resolved state, and nested replies. This
function is used by the comment-aware push flow when
gdocs-preserve-comments is non-nil (Comment-aware push).
The function gdocs-api-resolve-comment resolves a comment on a Google
Drive file via the Drive Comments API. It takes a FILE-ID and
COMMENT-ID and marks the comment as resolved by sending a PATCH request
that sets the resolved field to true. CALLBACK receives the updated
comment alist. This function is used by the comment-aware push flow
when the user chooses the “resolve & delete” action
(Comment-aware push).
The function gdocs-api-rename-file renames a Google Drive file
identified by FILE-ID to NEW-NAME. This is used internally by
gdocs-push when the local #+TITLE has changed since the last sync,
so the Google Doc title stays in sync with the org file
(Pushing and pulling).
The function gdocs-api-create-folder creates a new folder in Google
Drive. An optional PARENT-FOLDER-ID argument places the folder inside
a specific parent. This function is called by gdocs-create-folder
when building the Drive folder hierarchy for local directories
(Directory linking).
The function gdocs-api-move-file moves a Google Drive file into a
different folder by adding the file to the target folder and removing
it from its current parent. This is used by gdocs-create to place
newly created documents in the linked Drive folder
(Directory linking).
Format conversion
All conversion passes through a flat list of intermediate representation (IR) element plists, which serves as the canonical interchange format shared with the diff engine (Diff engine).
The function gdocs-convert-org-buffer-to-ir parses the current org
buffer using org-element-parse-buffer and returns an IR element list.
The related function gdocs-convert-org-string-to-ir does the same but
accepts an org-mode string rather than operating on the current buffer.
The function gdocs-convert-org-buffer-to-segments is a variant of
gdocs-convert-org-buffer-to-ir designed for the three-way merge.
Instead of returning a flat IR list, it returns a plist with four keys:
:ir (the full IR element list), :segments (a list of segment plists,
each pairing an :ir-element with its :org-text region), :preamble
(buffer text before the first body element, including the property
drawer and #+TITLE: line), and :postamble (text after the last
element). By preserving the mapping between IR
elements and their original org text, the three-way merge can retain
org-only metadata (property drawers, TODO keywords, tags) that would
otherwise be lost during conversion (Sync engine).
The function gdocs-convert-ir-to-org converts an IR element list back
to a well-formatted org-mode string. This is the inverse of
gdocs-convert-org-buffer-to-ir and is used when pulling remote
changes.
The function gdocs-convert-docs-json-to-ir converts a Google Docs API
JSON response (an alist with keys like body, title, lists, and
namedRanges) to an IR element list. It walks the document structure
and maps each structural element to its IR equivalent. Each resulting
IR element carries :doc-start and :doc-end properties copied from
the JSON’s startIndex and endIndex, giving the diff engine
authoritative UTF-16 document positions
(Intermediate representation).
The function gdocs-convert-ir-to-docs-requests converts an IR element
list to a list of Google Docs batchUpdate request alists. It tracks
a running UTF-16 insertion index starting at 1 (the beginning of the
Google Docs body). This function is used for full document replacement
and by the diff engine for incremental insertions.
Sync engine
Metadata storage
Each linked org file stores sync metadata in a file-level property drawer at the top of the file:
:PROPERTIES:
:GDOCS_DOCUMENT_ID: 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms
:GDOCS_ACCOUNT: personal
:GDOCS_REVISION_ID: ALm37BVT...
:GDOCS_LAST_SYNC: 2024-01-15T10:30:00+0000
:END:
The property drawer is the standard org-mode mechanism for
document-level metadata. All four properties are persisted to the
file, so revision tracking and last-sync timestamps survive across
Emacs sessions. The drawer is read via org-entry-get and written
via org-entry-put, using standard org APIs rather than the Emacs
file-local variable machinery.
Directories linked to Google Drive folders (Directory linking) store
their association in .dir-locals.el rather than per-file:
((org-mode . ((gdocs-folder-id . "1A2B3C4D5E6F...")
(gdocs-account . "personal"))))
This means every org file in the directory inherits the folder and
account settings. gdocs-create uses these values to place new
documents in the correct Drive folder.
Shadow copy
The sync engine maintains a buffer-local shadow IR: the intermediate representation of the last-synced state. This shadow enables incremental diffs on push and conflict detection on pull.
On the first open of a linked file after an Emacs restart, the shadow
is nil. The next push fetches the remote document and diffs against
it (rather than against the missing shadow), so incremental diffing
still works and comment anchors are preserved. If
gdocs-auto-pull-on-open is non-nil, the pull establishes the shadow
baseline automatically so that subsequent pushes diff against the
known-good shadow.
Push serialization
Pushes are serialized to prevent race conditions. If a push is already in flight when another is triggered (e.g., by a rapid save), the second push is queued and executes automatically when the first completes. This ensures that the shadow copy is always consistent with what was last sent to the API.
Post-push list nesting fixup
The diff engine emits createParagraphBullets requests for new list
items and for modifications that change the list type or nesting level.
These per-paragraph requests always produce flat (level-0) lists. The
full conversion path has a separate mechanism
(gdocs-convert--fixup-list-nesting) that groups bullets and inserts
leading tabs so that the API infers correct nesting levels, but the diff
path cannot use that mechanism because it interleaves modifications with
insertions at varying document positions.
When a content modification does not change the list structure (same
type and level), the diff engine suppresses the createParagraphBullets
request entirely. The trailing newline is preserved during paragraph
modifications, which keeps the paragraph in its existing Google Docs
list object. Re-emitting a bullet request with a fixed preset would
overwrite the alternated preset assigned during document creation and
cause Google Docs to merge separate numbered lists into one continuous
sequence.
To restore correct nesting, the sync engine runs a second
batchUpdate pass after the main push succeeds, but only when the
local IR contains nested list items (level > 0). The fixup proceeds
as follows:
Fetch the just-pushed document to obtain current paragraph ranges.
Align local and remote IR elements by plain-text content using the same LCS approach as the diff engine, so that each local element (which carries the correct
:levelfrom the org source) is paired with its remote counterpart (which carries:doc-start/:doc-endpositions).Build element-range plists that merge local list info with remote document positions. The local element’s
:listalways takes precedence, even when nil — this ensures that headings erroneously bulleted by the API are correctly treated as non-list elements by the guard logic in step 6.Identify contiguous list groups that contain at least one element at level > 0.
For each such group, emit four kinds of requests in order:
deleteParagraphBulletsto remove the flat bullets for the group range.updateParagraphStyleto resetindentStartandindentFirstLineto zero, so only leading tabs signal nesting depth.insertTextto insert N tab characters at each nested paragraph’sstartIndex(where N is the nesting level). Insertions are sorted by descending index to maintain correct positions.- A per-group
createParagraphBulletsrequest. The API counts the leading tabs, assigns the correct nesting levels, and removes the tabs afterward.
Emit defensive
deleteParagraphBulletsrequests for non-list paragraphs adjacent to the list groups. The Google Docs API can extend bullet formatting to heading paragraphs nearcreateParagraphBulletsranges, so these guards prevent headings from being accidentally turned into bullet points.
The fixup keeps the push lock held until completion, so any queued push waits until nesting is fully resolved before proceeding (Push serialization). If the local IR has no nested list items, the fixup is skipped entirely and the push completes in a single round trip.
Diff engine
The function gdocs-diff-generate is the main entry point of the diff
engine. It compares two IR element lists (OLD-IR and NEW-IR) and
returns a list of batchUpdate request plists representing the minimal
changes needed. Internally, it computes content-based keys for each
element, runs a longest-common-subsequence (LCS) algorithm to identify
kept, deleted, inserted, and modified elements, and generates the
appropriate API requests.
An optional START-INDEX argument specifies the UTF-16 code unit offset
where the first IR element begins in the document body (default 1).
The sync engine uses this to account for title elements that precede the
diffable body content (Sync engine). When the old IR carries
:doc-start / :doc-end properties (as produced by
gdocs-convert-docs-json-to-ir), the engine uses those authoritative
positions; otherwise it computes positions from element text lengths.
Requests are ordered for correct index stability: deletions and
modifications processed from highest index to lowest, with insertions
interleaved so that earlier operations do not shift indices of later
ones.
For workflows that need to inspect or filter diff operations before
generating API requests, two lower-level functions are available. The
function gdocs-diff-compute-operations returns the raw diff operation
list (with :op values of keep, delete, insert, or modify)
without generating requests. The function
gdocs-diff-generate-from-ops accepts a pre-computed operation list and
produces the corresponding API requests. The comment-aware push flow
uses this two-step API to filter declined deletions before generating
the final request batch (Comment-aware push).
Conflict resolution
When a pull detects that both local and remote changes exist, the
conflict resolution interface opens in a dedicated *gdocs-merge*
buffer using gdocs-merge-mode (Merge mode). The following commands
are available in the merge buffer:
gdocs-merge-next-hunk(n) moves to the next unresolved conflict hunk.gdocs-merge-previous-hunk(p) moves to the previous unresolved hunk.gdocs-merge-accept-local(a) accepts the local version of the current hunk.gdocs-merge-accept-remote(b) accepts the remote version of the current hunk.gdocs-merge-edit-hunk(e) opens a temporary org-mode buffer pre-filled with the local text so you can edit the merged result manually. PressC-c C-cin the edit buffer to accept.gdocs-merge-accept-all-local(A) resolves all unresolved hunks with the local version.gdocs-merge-accept-all-remote(B) resolves all unresolved hunks with the remote version.gdocs-merge-finalize(C-c C-c) finalizes the merge once all hunks are resolved, applies the merged content to the original buffer, and triggers a push. It signals an error if any hunks remain unresolved.gdocs-merge-abort(C-c C-korq) aborts the merge after confirmation, leaving the buffer unchanged.
Minor mode
The minor mode gdocs-mode is activated on org buffers linked to a
Google Doc. It provides:
- Automatic push on save (via
after-save-hook) whengdocs-auto-push-on-saveis non-nil. - Modeline status display showing the sync state:
GDocs:syncedwhen in sync.GDocs:modifiedwhen local changes have not been pushed.GDocs:pushingwhile a push is in progress.GDocs:conflictwhen remote changes conflict with local ones.GDocs:errorwhen the last sync failed.
- Keybindings under the
C-c gprefix (Keybindings).
The mode activates automatically when you open an org file that has a
GDOCS_DOCUMENT_ID property in its file-level property drawer (via a
hook on org-mode-hook). If buffers were restored at startup before
gdocs loaded (e.g. by desktop-save-mode), gdocs scans already-open
org buffers at load time and activates the mode retroactively. You
can also toggle it manually.
Keybindings
The gdocs-mode-map provides the following keybindings:
| Key | Command | Description |
|---|---|---|
C-c g p | gdocs-push | Push local changes |
C-c g l | gdocs-pull | Pull remote changes |
C-c g s | gdocs-status | Show sync status |
C-c g o | gdocs-open-in-browser | Open document in browser |
C-c g u | gdocs-unlink | Unlink from Google Doc |
Merge mode
gdocs-merge-mode is a major mode derived from special-mode used in
the *gdocs-merge* buffer during conflict resolution. Its keybindings
are documented in Conflict resolution.
The mode defines three faces for visual differentiation:
gdocs-merge-local-face(green background) for local changes.gdocs-merge-remote-face(red background) for remote changes.gdocs-merge-resolved-face(blue background) for resolved hunks.