notes

gdocs manual

First published: Last updated: 6391 words · 14 lines of code

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-mode minor 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

  1. Obtain OAuth credentials from Google Cloud Console (Obtaining credentials).

  2. Configure your credentials in Emacs:

    (setopt gdocs-accounts
            '(("personal" . ((client-id . "YOUR-CLIENT-ID")
                             (client-secret . "YOUR-CLIENT-SECRET")))))
    
  3. Authenticate: M-x gdocs-authenticate.

  4. Open an existing Google Doc: M-x gdocs-open.

  5. 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:

  1. Go to the Google Cloud Console and sign in with the Google account you want to use with gdocs.

  2. 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.
  3. 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”).
  4. 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/documents
      • https://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.
  5. 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.
  6. Configure gdocs with 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.

  7. Run M-x gdocs-authenticate. Your browser will open to a Google consent screen. Grant access, and gdocs will store the resulting tokens securely in gdocs-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).

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. Press C-c C-c in 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-k or q) 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), toggle gdocs-auto-pull-on-open (-l), set gdocs-directory (-d), set gdocs-org-tag (-t), set gdocs-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:

  1. Fetch the just-pushed document to obtain current paragraph ranges.

  2. 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 :level from the org source) is paired with its remote counterpart (which carries :doc-start / :doc-end positions).

  3. Build element-range plists that merge local list info with remote document positions. The local element’s :list always 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.

  4. Identify contiguous list groups that contain at least one element at level > 0.

  5. For each such group, emit four kinds of requests in order:

    • deleteParagraphBullets to remove the flat bullets for the group range.
    • updateParagraphStyle to reset indentStart and indentFirstLine to zero, so only leading tabs signal nesting depth.
    • insertText to insert N tab characters at each nested paragraph’s startIndex (where N is the nesting level). Insertions are sorted by descending index to maintain correct positions.
    • A per-group createParagraphBullets request. The API counts the leading tabs, assigns the correct nesting levels, and removes the tabs afterward.
  6. Emit defensive deleteParagraphBullets requests for non-list paragraphs adjacent to the list groups. The Google Docs API can extend bullet formatting to heading paragraphs near createParagraphBullets ranges, 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. Press C-c C-c in 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-k or q) 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) when gdocs-auto-push-on-save is non-nil.
  • Modeline status display showing the sync state:
    • GDocs:synced when in sync.
    • GDocs:modified when local changes have not been pushed.
    • GDocs:pushing while a push is in progress.
    • GDocs:conflict when remote changes conflict with local ones.
    • GDocs:error when the last sync failed.
  • Keybindings under the C-c g prefix (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:

KeyCommandDescription
C-c g pgdocs-pushPush local changes
C-c g lgdocs-pullPull remote changes
C-c g sgdocs-statusShow sync status
C-c g ogdocs-open-in-browserOpen document in browser
C-c g ugdocs-unlinkUnlink 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.

Indices

Function index

Variable index