gogcli/internal/cmd/docs_edit.go
2026-05-04 08:28:38 +01:00

837 lines
23 KiB
Go

package cmd
import (
"context"
"fmt"
"os"
"strings"
"github.com/alecthomas/kong"
"google.golang.org/api/docs/v1"
"google.golang.org/api/drive/v3"
gapi "google.golang.org/api/googleapi"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
// resolveTabArg returns the effective tab value from --tab or the deprecated
// --tab-id flag. It rejects supplying both and emits a deprecation warning
// when --tab-id is used.
func resolveTabArg(ctx context.Context, tab, tabID string) (string, error) {
tab = strings.TrimSpace(tab)
tabID = strings.TrimSpace(tabID)
if tab != "" && tabID != "" {
return "", usage("--tab and --tab-id are mutually exclusive (--tab-id is deprecated; use --tab)")
}
if tabID != "" {
u := ui.FromContext(ctx)
u.Err().Printf("Warning: --tab-id is deprecated; use --tab instead")
return tabID, nil
}
return tab, nil
}
type DocsWriteCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Text string `name:"text" help:"Text to write"`
File string `name:"file" help:"Text file path ('-' for stdin)"`
Replace bool `name:"replace" help:"Replace all content explicitly (required with --markdown unless --append is set)"`
Markdown bool `name:"markdown" help:"Convert markdown to Google Docs formatting (requires --replace or --append)"`
Append bool `name:"append" help:"Append instead of replacing the document body"`
Pageless bool `name:"pageless" help:"Set document to pageless mode"`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
Format DocsFormatFlags `embed:""`
}
func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootFlags) error {
id := strings.TrimSpace(c.DocID)
if id == "" {
return usage("empty docId")
}
text, err := c.resolveWriteText(kctx)
if err != nil {
return err
}
if c.Append && c.Replace {
return usage("--append cannot be combined with --replace")
}
tab, tabErr := resolveTabArg(ctx, c.Tab, c.TabID)
if tabErr != nil {
return tabErr
}
c.Tab = tab
if c.Markdown {
if c.Format.any() {
return usage("formatting flags are only supported for plain-text docs write; use markdown syntax or run docs format after writing")
}
return c.writeMarkdown(ctx, flags, id, text)
}
return c.writePlainText(ctx, flags, id, text)
}
func (c *DocsWriteCmd) resolveWriteText(kctx *kong.Context) (string, error) {
text, provided, err := resolveTextInput(c.Text, c.File, kctx, "text", "file")
if err != nil {
return "", err
}
if !provided {
return "", usage("required: --text or --file")
}
if text == "" {
return "", usage("empty text")
}
return text, nil
}
func (c *DocsWriteCmd) writePlainText(ctx context.Context, flags *RootFlags, docID, text string) error {
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
endIndex, tabID, err := docsTargetEndIndexAndTabID(ctx, svc, docID, c.Tab)
if err != nil {
return err
}
c.Tab = tabID
insertIndex := int64(1)
if c.Append {
insertIndex = docsAppendIndex(endIndex)
}
reqs, err := c.buildPlainWriteRequests(endIndex, insertIndex, text)
if err != nil {
return err
}
resp, err := svc.Documents.BatchUpdate(docID, &docs.BatchUpdateDocumentRequest{Requests: reqs}).Context(ctx).Do()
if err != nil {
if isDocsNotFound(err) {
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", docID)
}
return err
}
if err := c.applyPageless(ctx, svc, docID); err != nil {
return err
}
return c.writePlainTextResult(ctx, resp, len(reqs), insertIndex)
}
func (c *DocsWriteCmd) buildPlainWriteRequests(endIndex, insertIndex int64, text string) ([]*docs.Request, error) {
reqs := make([]*docs.Request, 0, 2)
if !c.Append {
deleteEnd := endIndex - 1
if deleteEnd > 1 {
reqs = append(reqs, &docs.Request{
DeleteContentRange: &docs.DeleteContentRangeRequest{
Range: &docs.Range{StartIndex: 1, EndIndex: deleteEnd, TabId: c.Tab},
},
})
}
}
reqs = append(reqs, &docs.Request{
InsertText: &docs.InsertTextRequest{
Location: &docs.Location{Index: insertIndex, TabId: c.Tab},
Text: text,
},
})
if c.Format.any() {
formatReqs, err := c.Format.buildRequests(insertIndex, insertIndex+utf16Len(text), c.Tab)
if err != nil {
return nil, err
}
reqs = append(reqs, formatReqs...)
}
return reqs, nil
}
func (c *DocsWriteCmd) applyPageless(ctx context.Context, svc *docs.Service, docID string) error {
if !c.Pageless {
return nil
}
if err := setDocumentPageless(ctx, svc, docID); err != nil {
return fmt.Errorf("set pageless mode: %w", err)
}
return nil
}
func (c *DocsWriteCmd) writePlainTextResult(ctx context.Context, resp *docs.BatchUpdateDocumentResponse, requestCount int, insertIndex int64) error {
u := ui.FromContext(ctx)
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": resp.DocumentId,
"requests": requestCount,
"append": c.Append,
"index": insertIndex,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
if resp.WriteControl != nil {
payload["writeControl"] = resp.WriteControl
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("id\t%s", resp.DocumentId)
u.Out().Printf("requests\t%d", requestCount)
u.Out().Printf("append\t%t", c.Append)
u.Out().Printf("index\t%d", insertIndex)
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
if resp.WriteControl != nil && resp.WriteControl.RequiredRevisionId != "" {
u.Out().Printf("revision\t%s", resp.WriteControl.RequiredRevisionId)
}
return nil
}
func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docID, content string) error {
u := ui.FromContext(ctx)
if c.Append {
return c.appendMarkdown(ctx, flags, docID, content)
}
if !c.Replace {
return usage("--markdown requires --replace or --append")
}
// Tab support for markdown replace is limited because Drive's markdown
// converter doesn't support tab-specific updates, so we skip tab support here.
if c.Tab != "" {
return usage("--markdown with --replace does not support --tab (Drive's markdown converter operates on entire documents)")
}
cleaned, images := extractMarkdownImages(content)
account, driveSvc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
updated, err := driveSvc.Files.Update(docID, &drive.File{}).
Media(strings.NewReader(cleaned), gapi.ContentType(mimeTextMarkdown)).
SupportsAllDrives(true).
Fields("id,name,webViewLink").
Context(ctx).
Do()
if err != nil {
return fmt.Errorf("writing markdown to document: %w", err)
}
var docsSvc *docs.Service
if len(images) > 0 || c.Pageless {
var svcErr error
docsSvc, svcErr = newDocsService(ctx, account)
if svcErr != nil {
return svcErr
}
}
if len(images) > 0 {
if err := insertImagesIntoDocs(ctx, docsSvc, docID, images, ""); err != nil {
cleanupDocsImagePlaceholders(ctx, docsSvc, docID, images, "")
return fmt.Errorf("insert images: %w", err)
}
}
if c.Pageless {
if err := c.applyPageless(ctx, docsSvc, docID); err != nil {
return err
}
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": updated.Id,
"written": len(content),
"replaced": true,
"markdown": true,
}
if c.Pageless {
payload["pageless"] = true
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("documentId\t%s", updated.Id)
u.Out().Printf("written\t%d", len(content))
u.Out().Printf("mode\treplaced (markdown converted)")
if c.Pageless {
u.Out().Printf("pageless\ttrue")
}
if updated.WebViewLink != "" {
u.Out().Printf("link\t%s", updated.WebViewLink)
}
return nil
}
func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, docID, content string) error {
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
endIndex, tabID, err := docsTargetEndIndexAndTabID(ctx, svc, docID, c.Tab)
if err != nil {
return err
}
c.Tab = tabID
insertIndex := docsAppendIndex(endIndex)
requestCount, inserted, err := insertDocsMarkdownAt(ctx, svc, docID, insertIndex, content, c.Tab)
if err != nil {
if isDocsNotFound(err) {
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", docID)
}
return err
}
if err := c.applyPageless(ctx, svc, docID); err != nil {
return err
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": docID,
"written": inserted,
"requests": requestCount,
"append": true,
"index": insertIndex,
"markdown": true,
}
if c.Pageless {
payload["pageless"] = true
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u := ui.FromContext(ctx)
u.Out().Printf("documentId\t%s", docID)
u.Out().Printf("written\t%d", inserted)
u.Out().Printf("requests\t%d", requestCount)
u.Out().Printf("mode\tappended (markdown converted)")
u.Out().Printf("index\t%d", insertIndex)
if c.Pageless {
u.Out().Printf("pageless\ttrue")
}
return nil
}
type DocsUpdateCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Text string `name:"text" help:"Text to insert"`
File string `name:"file" help:"Text file path ('-' for stdin)"`
Index int64 `name:"index" help:"Insert index (default: end of document)"`
Pageless bool `name:"pageless" help:"Set document to pageless mode"`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
}
func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
id := strings.TrimSpace(c.DocID)
if id == "" {
return usage("empty docId")
}
text, provided, err := resolveTextInput(c.Text, c.File, kctx, "text", "file")
if err != nil {
return err
}
if !provided {
return usage("required: --text or --file")
}
if text == "" {
return usage("empty text")
}
if flagProvided(kctx, "index") && c.Index <= 0 {
return usage("invalid --index (must be >= 1)")
}
tab, tabErr := resolveTabArg(ctx, c.Tab, c.TabID)
if tabErr != nil {
return tabErr
}
c.Tab = tab
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
insertIndex := c.Index
if insertIndex <= 0 {
endIndex, tabID, endErr := docsTargetEndIndexAndTabID(ctx, svc, id, c.Tab)
if endErr != nil {
return endErr
}
c.Tab = tabID
insertIndex = docsAppendIndex(endIndex)
} else if c.Tab != "" {
tabID, tabErr := resolveDocsTabID(ctx, svc, id, c.Tab)
if tabErr != nil {
return tabErr
}
c.Tab = tabID
}
reqs := []*docs.Request{{
InsertText: &docs.InsertTextRequest{
Location: &docs.Location{Index: insertIndex, TabId: c.Tab},
Text: text,
},
}}
resp, err := svc.Documents.BatchUpdate(id, &docs.BatchUpdateDocumentRequest{Requests: reqs}).Context(ctx).Do()
if err != nil {
if isDocsNotFound(err) {
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", id)
}
return err
}
if c.Pageless {
if err := setDocumentPageless(ctx, svc, id); err != nil {
return fmt.Errorf("set pageless mode: %w", err)
}
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": resp.DocumentId,
"requests": len(reqs),
"index": insertIndex,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
if resp.WriteControl != nil {
payload["writeControl"] = resp.WriteControl
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("id\t%s", resp.DocumentId)
u.Out().Printf("requests\t%d", len(reqs))
u.Out().Printf("index\t%d", insertIndex)
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
if resp.WriteControl != nil && resp.WriteControl.RequiredRevisionId != "" {
u.Out().Printf("revision\t%s", resp.WriteControl.RequiredRevisionId)
}
return nil
}
type DocsInsertCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Content string `arg:"" optional:"" name:"content" help:"Text to insert (or use --file / stdin)"`
Index int64 `name:"index" help:"Character index to insert at (1 = beginning)" default:"1"`
File string `name:"file" short:"f" help:"Read content from file (use - for stdin)"`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
}
func (c *DocsInsertCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := strings.TrimSpace(c.DocID)
if docID == "" {
return usage("empty docId")
}
content, err := resolveContentInput(c.Content, c.File)
if err != nil {
return err
}
if content == "" {
return usage("no content provided (use argument, --file, or stdin)")
}
if c.Index < 1 {
return usage("--index must be >= 1 (index 0 is reserved)")
}
tab, tabErr := resolveTabArg(ctx, c.Tab, c.TabID)
if tabErr != nil {
return tabErr
}
c.Tab = tab
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
if c.Tab != "" {
tabID, tabErr := resolveDocsTabID(ctx, svc, docID, c.Tab)
if tabErr != nil {
return tabErr
}
c.Tab = tabID
}
result, err := svc.Documents.BatchUpdate(docID, &docs.BatchUpdateDocumentRequest{
Requests: []*docs.Request{{
InsertText: &docs.InsertTextRequest{
Text: content,
Location: &docs.Location{
Index: c.Index,
TabId: c.Tab,
},
},
}},
}).Context(ctx).Do()
if err != nil {
return fmt.Errorf("inserting text: %w", err)
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{"documentId": result.DocumentId, "inserted": len(content), "atIndex": c.Index}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("documentId\t%s", result.DocumentId)
u.Out().Printf("inserted\t%d bytes", len(content))
u.Out().Printf("atIndex\t%d", c.Index)
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
return nil
}
type DocsDeleteCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Start int64 `name:"start" required:"" help:"Start index (>= 1)"`
End int64 `name:"end" required:"" help:"End index (> start)"`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
}
func (c *DocsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := strings.TrimSpace(c.DocID)
if docID == "" {
return usage("empty docId")
}
if c.Start < 1 {
return usage("--start must be >= 1")
}
if c.End <= c.Start {
return usage("--end must be greater than --start")
}
tab, tabErr := resolveTabArg(ctx, c.Tab, c.TabID)
if tabErr != nil {
return tabErr
}
c.Tab = tab
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
if c.Tab != "" {
tabID, tabErr := resolveDocsTabID(ctx, svc, docID, c.Tab)
if tabErr != nil {
return tabErr
}
c.Tab = tabID
}
result, err := svc.Documents.BatchUpdate(docID, &docs.BatchUpdateDocumentRequest{
Requests: []*docs.Request{{
DeleteContentRange: &docs.DeleteContentRangeRequest{
Range: &docs.Range{StartIndex: c.Start, EndIndex: c.End, TabId: c.Tab},
},
}},
}).Context(ctx).Do()
if err != nil {
return fmt.Errorf("deleting content: %w", err)
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": result.DocumentId,
"deleted": c.End - c.Start,
"startIndex": c.Start,
"endIndex": c.End,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("documentId\t%s", result.DocumentId)
u.Out().Printf("deleted\t%d characters", c.End-c.Start)
u.Out().Printf("range\t%d-%d", c.Start, c.End)
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
return nil
}
type DocsClearCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
}
func (c *DocsClearCmd) Run(ctx context.Context, flags *RootFlags) error {
docID := strings.TrimSpace(c.DocID)
if docID == "" {
return usage("empty docId")
}
return (&DocsSedCmd{DocID: docID, Expression: `s/^$//`}).Run(ctx, flags)
}
type DocsFindReplaceCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Find string `arg:"" name:"find" help:"Text to find"`
ReplaceText string `arg:"" optional:"" name:"replace" help:"Replacement text (omit when using --content-file)"`
ContentFile string `name:"content-file" help:"Read replacement from a file instead of the positional argument."`
MatchCase bool `name:"match-case" help:"Case-sensitive matching"`
Format string `name:"format" help:"Replacement format: plain|markdown. Markdown converts formatting, tables, and inline images from public HTTPS URLs." default:"plain" enum:"plain,markdown"`
First bool `name:"first" help:"Replace only the first occurrence instead of all."`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
}
type DocsEditCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Find string `arg:"" name:"find" help:"Text to find"`
ReplaceStr string `arg:"" name:"replace" help:"Replacement text"`
MatchCase bool `name:"match-case" help:"Case-sensitive matching"`
}
func (c *DocsEditCmd) Run(ctx context.Context, flags *RootFlags) error {
return (&DocsFindReplaceCmd{
DocID: c.DocID,
Find: c.Find,
ReplaceText: c.ReplaceStr,
MatchCase: c.MatchCase,
}).Run(ctx, flags)
}
func (c *DocsFindReplaceCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := strings.TrimSpace(c.DocID)
if docID == "" {
return usage("empty docId")
}
if c.Find == "" {
return usage("find text cannot be empty")
}
replaceText, err := c.resolveReplaceText()
if err != nil {
return err
}
format := strings.ToLower(strings.TrimSpace(c.Format))
if format == "" {
format = docsContentFormatPlain
}
tab, tabErr := resolveTabArg(ctx, c.Tab, c.TabID)
if tabErr != nil {
return tabErr
}
c.Tab = tab
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
if c.Tab != "" {
tabID, tabErr := resolveDocsTabID(ctx, svc, docID, c.Tab)
if tabErr != nil {
return tabErr
}
c.Tab = tabID
}
if flags != nil && flags.DryRun {
return c.runDryRun(ctx, u, svc, docID, replaceText, format)
}
if !c.First && format == docsContentFormatPlain {
return c.runReplaceAll(ctx, u, svc, docID, replaceText)
}
loaded, err := loadDocsTargetDocument(ctx, svc, docID, c.Tab)
if err != nil {
return err
}
c.Tab = loaded.tabID
doc := loaded.full
targetDoc := loaded.target
if c.First {
startIdx, endIdx, total := findTextInDoc(targetDoc, c.Find, c.MatchCase)
if total == 0 {
return c.printFirstResult(ctx, u, docID, replaceText, 0, 0)
}
if format == docsContentFormatMarkdown {
err = c.runMarkdown(ctx, svc, doc, startIdx, endIdx, replaceText)
} else {
err = c.runPlain(ctx, svc, doc, startIdx, endIdx, replaceText)
}
if err != nil {
return err
}
return c.printFirstResult(ctx, u, docID, replaceText, 1, total)
}
matches := findTextMatches(targetDoc, c.Find, c.MatchCase)
for i := len(matches) - 1; i >= 0; i-- {
if err = c.runMarkdown(ctx, svc, doc, matches[i].startIndex, matches[i].endIndex, replaceText); err != nil {
return err
}
if i == 0 {
continue
}
loaded, err = loadDocsTargetDocument(ctx, svc, docID, c.Tab)
if err != nil {
return fmt.Errorf("re-reading document: %w", err)
}
doc = loaded.full
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": docID,
"find": c.Find,
"replace": replaceText,
"replacements": len(matches),
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("documentId\t%s", docID)
u.Out().Printf("find\t%s", c.Find)
u.Out().Printf("replace\t%s", replaceText)
u.Out().Printf("replacements\t%d", len(matches))
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
return nil
}
func (c *DocsFindReplaceCmd) runDryRun(ctx context.Context, u *ui.UI, svc *docs.Service, docID, replaceText, format string) error {
loaded, err := loadDocsTargetDocument(ctx, svc, docID, c.Tab)
if err != nil {
return err
}
c.Tab = loaded.tabID
matches := findTextMatches(loaded.target, c.Find, c.MatchCase)
replacements := len(matches)
if c.First && replacements > 1 {
replacements = 1
}
remaining := len(matches) - replacements
payload := map[string]any{
"documentId": docID,
"find": c.Find,
"replace": replaceText,
"format": format,
"first": c.First,
"replacements": replacements,
"remaining": remaining,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
if err := dryRunExit(ctx, &RootFlags{DryRun: true}, "docs.find-replace", payload); err != nil {
return err
}
if !outfmt.IsJSON(ctx) {
u.Out().Printf("matches\t%d", len(matches))
}
return nil
}
func (c *DocsFindReplaceCmd) runReplaceAll(ctx context.Context, u *ui.UI, svc *docs.Service, docID, replaceText string) error {
documentID, replacements, err := runDocsReplaceAll(ctx, svc, docID, c.Find, replaceText, c.MatchCase, c.Tab)
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": documentID,
"find": c.Find,
"replace": replaceText,
"replacements": replacements,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("documentId\t%s", documentID)
u.Out().Printf("find\t%s", c.Find)
u.Out().Printf("replace\t%s", replaceText)
u.Out().Printf("replacements\t%d", replacements)
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
return nil
}
func (c *DocsFindReplaceCmd) runPlain(ctx context.Context, svc *docs.Service, doc *docs.Document, startIdx, endIdx int64, replaceText string) error {
return replaceDocsTextRange(ctx, svc, doc, startIdx, endIdx, replaceText, c.Tab)
}
func (c *DocsFindReplaceCmd) runMarkdown(ctx context.Context, svc *docs.Service, doc *docs.Document, startIdx, endIdx int64, replaceText string) error {
return replaceDocsMarkdownRange(ctx, svc, doc, startIdx, endIdx, replaceText, c.Tab)
}
func (c *DocsFindReplaceCmd) printFirstResult(ctx context.Context, u *ui.UI, docID, replaceText string, replacements, total int) error {
if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": docID,
"find": c.Find,
"replacements": replacements,
"remaining": total - replacements,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}
u.Out().Printf("documentId\t%s", docID)
u.Out().Printf("find\t%s", c.Find)
u.Out().Printf("replace\t%s", replaceText)
u.Out().Printf("replacements\t%d", replacements)
if remaining := total - replacements; remaining > 0 {
u.Out().Printf("remaining\t%d", remaining)
}
if c.Tab != "" {
u.Out().Printf("tabId\t%s", c.Tab)
}
return nil
}
func (c *DocsFindReplaceCmd) resolveReplaceText() (string, error) {
if c.ContentFile != "" && c.ReplaceText != "" {
return "", usage("cannot use both replace argument and --content-file")
}
if c.ContentFile == "" {
return c.ReplaceText, nil
}
data, err := os.ReadFile(c.ContentFile)
if err != nil {
return "", fmt.Errorf("read content file: %w", err)
}
return string(data), nil
}