diff --git a/CHANGELOG.md b/CHANGELOG.md index 0492a7a3..1e10c293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Choosing a monospace font for the interface in Settings › Appearance asks whether the terminal and the editor should use it too. Use everywhere sets both; Interface only leaves them as they were. A proportional font or System asks nothing. +- A comment waiting in the review batch can be reworded before it is sent: its pencil opens it in a dialog, saved with Save or Ctrl+Enter. A line comment in a pull request's pending review has an Edit button beside Discard that opens it in a box, saved with Save or Ctrl+Enter. ### Removed diff --git a/native/cmd/lich-native/dock_comment_edit.go b/native/cmd/lich-native/dock_comment_edit.go new file mode 100644 index 00000000..e066c72c --- /dev/null +++ b/native/cmd/lich-native/dock_comment_edit.go @@ -0,0 +1,76 @@ +package main + +import ( + "path" + "strings" + + "gioui.org/io/key" + "gioui.org/layout" + "gioui.org/widget" + + "github.com/lichdotdev/lich/native/i18n" + "github.com/lichdotdev/lich/native/ui" +) + +// commentEditDialog rewords the batch comment a pencil was clicked on, in a +// textarea big enough for a long one. One per window: every surface's batch +// opens it through the shared commentStore. Ctrl+Enter saves, Enter is a new +// line. +type commentEditDialog struct { + dialog ui.DialogState + editor widget.Editor + resize ui.TextareaResize + save, cancel widget.Clickable + // shown is the comment the editor was filled from, and where is its + // anchor, kept so the panel still reads right while it closes. + shown int + where string +} + +// layout is laid out by the window before its content, as every dialog is. +func (d *commentEditDialog) layout(gtx C, th *ui.Theme, store *commentStore) { + c, open := store.edited() + if open && d.shown != c.id { + d.fill(gtx, c) + } + text := strings.TrimSpace(d.editor.Text()) + if (d.save.Clicked(gtx) || ctrlEnter(gtx, &d.editor)) && open && text != "" { + store.edit(store.editing.target, c.id, text) + open = false + } + if d.cancel.Clicked(gtx) { + open = false + } + on := th.On(th.Popover) + cancel := on.Button(&d.cancel, i18n.T("common.action.cancel")) + cancel.Variant = ui.VariantOutline + save := on.Button(&d.save, i18n.T("diff.commentEdit.save")) + save.Disabled = text == "" + dlg := th.Dialog(&d.dialog, open, i18n.T("diff.commentEdit.title"), d.where) + dlg.MaxWidth = ui.DialogWidthXL + dlg.Body = func(gtx C) D { + gtx.Constraints.Max.Y = min(gtx.Constraints.Max.Y, gtx.Dp(ui.Space(64))) + ta := on.Textarea(&d.editor, "") + ta.Resize = &d.resize + return ta.Layout(gtx) + } + dlg.Actions = []layout.Widget{cancel.Layout, save.Layout} + if dlg.Layout(gtx) { + open = false + } + if !open { + store.endEdit() + d.shown = 0 + } +} + +func (d *commentEditDialog) fill(gtx C, c reviewComment) { + d.shown = c.id + d.where = "" + if c.path != "" { + d.where = path.Base(c.path) + ":" + c.lines + } + d.editor.SetText(c.txt) + d.editor.SetCaret(d.editor.Len(), d.editor.Len()) + gtx.Execute(key.FocusCmd{Tag: &d.editor}) +} diff --git a/native/cmd/lich-native/dock_comments.go b/native/cmd/lich-native/dock_comments.go index ab6eeb51..5cae9abe 100644 --- a/native/cmd/lich-native/dock_comments.go +++ b/native/cmd/lich-native/dock_comments.go @@ -9,6 +9,7 @@ package main // comments := new(commentStore) // one per window // comments.add(target, path, lines, text) // target is the checkout path // comments.list(target) // what the strip shows +// comments.beginEdit(target, id) // a row's pencil; the window's commentEditDialog opens on it // // var c commentComposer // one per surface // c.open(path, lines) // "Comment for the session" chosen on lines "12-30" @@ -43,6 +44,7 @@ import ( var ( icBatchRemove = icons.Lucide("x") + icBatchEdit = icons.Lucide("pencil") icBatchSend = icons.Lucide("send") ) @@ -59,6 +61,14 @@ type reviewComment struct { type commentStore struct { byTarget map[string][]reviewComment next int + // editing is the comment the window's commentEditDialog is open on. + editing commentRef +} + +// commentRef names one comment of one checkout's batch; id 0 is none. +type commentRef struct { + target string + id int } // add appends a comment; blank text is not a comment and is dropped. @@ -81,8 +91,33 @@ func (s *commentStore) remove(target string, id int) { s.byTarget[target] = slices.DeleteFunc(s.byTarget[target], func(c reviewComment) bool { return c.id == id }) } +// edit rewords a comment in place, keeping where it points. +func (s *commentStore) edit(target string, id int, text string) { + for i, c := range s.byTarget[target] { + if c.id == id { + s.byTarget[target][i].txt = strings.TrimSpace(text) + } + } +} + func (s *commentStore) clear(target string) { delete(s.byTarget, target) } +func (s *commentStore) beginEdit(target string, id int) { s.editing = commentRef{target, id} } + +func (s *commentStore) endEdit() { s.editing = commentRef{} } + +func (s *commentStore) isEditing() bool { return s.editing.id != 0 } + +// edited is the comment being edited, if it is still in its batch. +func (s *commentStore) edited() (reviewComment, bool) { + for _, c := range s.byTarget[s.editing.target] { + if c.id == s.editing.id { + return c, true + } + } + return reviewComment{}, false +} + // list is target's batch in the order it was written. The slice is the // store's: read it, don't keep it across a change. func (s *commentStore) list(target string) []reviewComment { return s.byTarget[target] } @@ -263,10 +298,12 @@ func (c *commentComposer) box(th *ui.Theme) layout.Widget { // commentBatch is the strip at the foot of a review panel: the comments so // far, a note about the whole change, and the one action that hands them all -// to the session as one prompt. +// to the session as one prompt. A comment's pencil opens the window's +// commentEditDialog on it. type commentBatch struct { list widget.List removes map[int]*widget.Clickable + edits map[int]*widget.Clickable note widget.Editor clear, send widget.Clickable // sending is the batch on its way to the session, still in the store: @@ -314,6 +351,9 @@ func (b *commentBatch) layout(gtx C, th *ui.Theme, store *commentStore, target s // update runs the strip's clicks against the store before it is drawn. func (b *commentBatch) update(gtx C, store *commentStore, target string, submit func(string, func(error)) bool, notify func(ui.Toast)) { b.note.SingleLine, b.note.Submit = true, true + if b.removes == nil { + b.removes, b.edits = map[int]*widget.Clickable{}, map[int]*widget.Clickable{} + } for { ev, ok := b.note.Update(gtx) if !ok { @@ -328,6 +368,12 @@ func (b *commentBatch) update(gtx C, store *commentStore, target string, submit if c.Clicked(gtx) { store.remove(target, id) delete(b.removes, id) + delete(b.edits, id) + } + } + for id, c := range b.edits { + if c.Clicked(gtx) { + store.beginEdit(target, id) } } if b.clear.Clicked(gtx) { @@ -384,18 +430,6 @@ func (b *commentBatch) drop(store *commentStore, target string) { b.note.SetText("") } -func (b *commentBatch) remove(id int) *widget.Clickable { - if b.removes == nil { - b.removes = map[int]*widget.Clickable{} - } - c, ok := b.removes[id] - if !ok { - c = new(widget.Clickable) - b.removes[id] = c - } - return c -} - func (b *commentBatch) rows(gtx C, th *ui.Theme, comments []reviewComment) D { b.list.Axis = layout.Vertical return th.ScrollArea(&b.list).Layout(gtx, len(comments), func(gtx C, i int) D { @@ -403,10 +437,13 @@ func (b *commentBatch) rows(gtx C, th *ui.Theme, comments []reviewComment) D { }) } -// row is one comment: where (mono, muted), what (truncated), and its ×. +// row is one comment: where (mono, muted), what (truncated), its pencil and +// its ×. func (b *commentBatch) row(gtx C, th *ui.Theme, c reviewComment) D { gtx.Constraints.Min.X = gtx.Constraints.Max.X - remove := th.Button(b.remove(c.id), "") + edit := th.Button(clickableAt(b.edits, c.id), "") + edit.Icon, edit.Variant, edit.Size = icBatchEdit, ui.VariantGhost, ui.SizeIconXS + remove := th.Button(clickableAt(b.removes, c.id), "") remove.Icon, remove.Variant, remove.Size = icBatchRemove, ui.VariantGhost, ui.SizeIconXS return layout.Inset{Left: ui.Space(1.5), Right: ui.Space(1.5), Top: ui.Space(1), Bottom: ui.Space(1)}.Layout(gtx, func(gtx C) D { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, @@ -423,6 +460,7 @@ func (b *commentBatch) row(gtx C, th *ui.Theme, c reviewComment) D { return l.Layout(gtx) }), ui.Gap(2), + layout.Rigid(edit.Layout), layout.Rigid(remove.Layout), ) }) diff --git a/native/cmd/lich-native/dock_comments_test.go b/native/cmd/lich-native/dock_comments_test.go index 2b66d627..cf2c2550 100644 --- a/native/cmd/lich-native/dock_comments_test.go +++ b/native/cmd/lich-native/dock_comments_test.go @@ -101,3 +101,33 @@ func TestTakenBatchDropsOnlyWhatWasSent(t *testing.T) { t.Fatalf("batch after the send was taken = %+v, want only the later comment", got) } } + +func TestCommentBatchEditsOneInPlace(t *testing.T) { + var s commentStore + s.add("/repo", "a.go", "1", "first") + s.add("/repo", "b.go", "2", "second") + s.edit("/repo", s.list("/repo")[0].id, " first, reworded ") + got := s.list("/repo") + if len(got) != 2 || got[0].txt != "first, reworded" || got[0].path != "a.go" || got[0].lines != "1" || got[1].txt != "second" { + t.Fatalf("after edit = %+v, want the first reworded and trimmed, in place, the second untouched", got) + } +} + +func TestCommentEditFollowsTheCommentItWasOpenedOn(t *testing.T) { + var s commentStore + s.add("/repo", "a.go", "1", "first") + s.add("/repo", "b.go", "2", "second") + second := s.list("/repo")[1].id + s.beginEdit("/repo", second) + if c, ok := s.edited(); !ok || c.txt != "second" || !s.isEditing() { + t.Fatalf("edited() = %+v, %v; want the second comment", c, ok) + } + s.remove("/repo", second) + if _, ok := s.edited(); ok { + t.Fatal("edited() still finds a removed comment") + } + s.endEdit() + if s.isEditing() { + t.Fatal("still editing after endEdit") + } +} diff --git a/native/cmd/lich-native/main.go b/native/cmd/lich-native/main.go index 64dca40b..f3dd7331 100644 --- a/native/cmd/lich-native/main.go +++ b/native/cmd/lich-native/main.go @@ -104,10 +104,13 @@ type window struct { model *model terms *terminals review *reviewPanel - dock *dock - sidebar *sidebar - footer *footer - topbar *topbar + // comments is the review batch every surface shares. + comments commentStore + commentEdit commentEditDialog + dock *dock + sidebar *sidebar + footer *footer + topbar *topbar // settings is the Settings screen, drawn over the stage while open. settings *settingsScreen gate *spawnGate @@ -251,7 +254,7 @@ func run(w *app.Window, rt lichclient.Runtime) error { u.footer = newFooter(ctx, client, &u.root, u.model.usage, u.model.quota, w.Invalidate) u.gate = &spawnGate{probe: client, invalidate: w.Invalidate, park: u.parkSession, notify: func(t ui.Toast) { u.toaster.Show(t) }} u.footer.toaster = &u.toaster - comments, notify := new(commentStore), func(t ui.Toast) { u.toaster.Show(t) } + comments, notify := &u.comments, func(t ui.Toast) { u.toaster.Show(t) } if u.review, err = newReviewPanel(client, &u.root, comments, *flagFont, unit.Sp(*flagSize), w.Invalidate, notify); err != nil { return err } @@ -490,6 +493,7 @@ func (u *window) layout(gtx C) error { u.layoutSubmitDialog(gtx, u.theme) u.layoutMergeDialog(gtx, u.theme) u.review.layoutDialogs(gtx, u.theme) + u.commentEdit.layout(gtx, u.theme, &u.comments) u.settings.theme.layoutDialogs(gtx, u.theme) u.settings.layoutFontReplication(gtx, u.theme) u.layoutProviderSetup(gtx, u.theme) @@ -806,7 +810,7 @@ func (u *window) lostNotice(gtx C) D { // this frame: not while a field or a dialog over it is typing. func (u *window) terminalTakesKeys(gtx C) bool { typing := u.sidebar.holdsKeys() || u.footer.holdsKeys() || u.dock.holdsKeys(gtx) || u.topbar.holdsKeys() - return !typing && !u.palette.open && !u.worktree.open + return !typing && !u.palette.open && !u.worktree.open && !u.comments.isEditing() } func (u *window) apply(act sidebarAction) { diff --git a/native/cmd/lich-native/pulls_conversation.go b/native/cmd/lich-native/pulls_conversation.go index fd42474d..1828cb91 100644 --- a/native/cmd/lich-native/pulls_conversation.go +++ b/native/cmd/lich-native/pulls_conversation.go @@ -31,6 +31,18 @@ type talkState struct { commentSize ui.TextareaResize commentSend widget.Clickable discards map[int]*widget.Clickable + edits map[int]*widget.Clickable + pendingEdit pendingEdit +} + +// pendingEdit is the box rewording one comment of the pending review, by its +// index there. +type pendingEdit struct { + open bool + index int + editor widget.Editor + resize ui.TextareaResize + save, cancel widget.Clickable } func (s *talkState) init() { @@ -38,7 +50,7 @@ func (s *talkState) init() { s.replying, s.replyFields = map[string]bool{}, map[string]*replyField{} s.replyClicks, s.sendClicks = map[string]*widget.Clickable{}, map[string]*widget.Clickable{} s.cancelClicks, s.resolveClick = map[string]*widget.Clickable{}, map[string]*widget.Clickable{} - s.discards = map[int]*widget.Clickable{} + s.discards, s.edits = map[int]*widget.Clickable{}, map[int]*widget.Clickable{} } // replyField is one thread's reply box: its text and the height it was @@ -66,11 +78,31 @@ func (v *pullView) talkClicks(gtx C, pending *pendingReview) { if s.resolvedTap.Clicked(gtx) { s.showResolved.Value = !s.showResolved.Value } - for i := range pending.snapshot() { + for i, c := range pending.snapshot() { if discard := s.discards[i]; discard != nil && discard.Clicked(gtx) { pending.remove(i) + // Discarding shifts the indices the edit box was opened by. + s.pendingEdit.open = false + } + if edit := s.edits[i]; edit != nil && edit.Clicked(gtx) { + s.pendingEdit.open, s.pendingEdit.index = true, i + s.pendingEdit.editor.SetText(c.Body) + gtx.Execute(keyFocus(&s.pendingEdit.editor)) } } + s.pendingEdit.clicks(gtx, pending) +} + +// clicks files the box's text on Save or Ctrl+Enter, or closes it on Cancel. +func (e *pendingEdit) clicks(gtx C, pending *pendingReview) { + text := strings.TrimSpace(e.editor.Text()) + if (e.save.Clicked(gtx) || ctrlEnter(gtx, &e.editor)) && e.open && text != "" { + pending.edit(e.index, text) + e.open = false + } + if e.cancel.Clicked(gtx) { + e.open = false + } } // conversationRows is PullsConversation: the pending review, the timeline, @@ -405,31 +437,67 @@ func (v *pullView) pendingRows(th *ui.Theme, r *pendingReview) []layout.Widget { }) }} for i, c := range comments { - discard := v.talk.discards[i] - if discard == nil { - discard = new(widget.Clickable) - v.talk.discards[i] = discard - } - rows = append(rows, func(gtx C) D { - anchor := lineAnchor(c.StartLine, c.Line) - b := th.Button(discard, i18n.T("pulls.pullsConversation.discard")) - b.Size, b.Variant = ui.SizeXS, ui.VariantGhost - return layout.Inset{Left: pullsPadX, Right: pullsPadX, Top: ui.Space(2)}.Layout(gtx, func(gtx C) D { - return layout.Flex{}.Layout(gtx, - layout.Flexed(1, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(th.Text(ui.TextXS, c.Path+" · "+anchor).Mono().Muted().Layout), - layout.Rigid(func(gtx C) D { return wrapped(gtx, th.Text(ui.TextSM, c.Body)) }), - ) - }), - layout.Rigid(b.Layout), - ) - }) - }) + rows = append(rows, func(gtx C) D { return v.pendingRow(gtx, th, i, c) }) } return rows } +// pendingRow is one line comment of the review being written, with Edit and +// Discard, or the box rewording it. +func (v *pullView) pendingRow(gtx C, th *ui.Theme, i int, c lichclient.ReviewComment) D { + s := &v.talk + discard, edit := clickableAt(s.discards, i), clickableAt(s.edits, i) + editing := s.pendingEdit.open && s.pendingEdit.index == i + body := func(gtx C) D { return wrapped(gtx, th.Text(ui.TextSM, c.Body)) } + if editing { + body = func(gtx C) D { return v.pendingEditBox(gtx, th) } + } + editB := th.Button(edit, i18n.T("pulls.pullsConversation.edit")) + editB.Size, editB.Variant = ui.SizeXS, ui.VariantGhost + discardB := th.Button(discard, i18n.T("pulls.pullsConversation.discard")) + discardB.Size, discardB.Variant = ui.SizeXS, ui.VariantGhost + anchor := lineAnchor(c.StartLine, c.Line) + return layout.Inset{Left: pullsPadX, Right: pullsPadX, Top: ui.Space(2)}.Layout(gtx, func(gtx C) D { + return layout.Flex{}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(th.Text(ui.TextXS, c.Path+" · "+anchor).Mono().Muted().Layout), + layout.Rigid(body), + ) + }), + layout.Rigid(func(gtx C) D { + if editing { + return D{} + } + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(editB.Layout), ui.Gap(1), layout.Rigid(discardB.Layout)) + }), + ) + }) +} + +// pendingEditBox is the reply box's shape over the comment being reworded; +// pendingEdit.clicks carries out its buttons. +func (v *pullView) pendingEditBox(gtx C, th *ui.Theme) D { + e := &v.talk.pendingEdit + saveB := th.Button(&e.save, i18n.T("pulls.pullsConversation.save")) + saveB.Size, saveB.Disabled = ui.SizeXS, strings.TrimSpace(e.editor.Text()) == "" + cancelB := th.Button(&e.cancel, i18n.T("common.action.cancel")) + cancelB.Size, cancelB.Variant = ui.SizeXS, ui.VariantGhost + return layout.Inset{Top: ui.Space(1)}.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx C) D { + ta := th.Textarea(&e.editor, "") + ta.Resize = &e.resize + return ta.Layout(gtx) + }), + ui.VGap(2), + layout.Rigid(func(gtx C) D { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Rigid(saveB.Layout), ui.Gap(2), layout.Rigid(cancelB.Layout)) + }), + ) + }) +} + func (v *pullView) pendingOf(url string) *pendingReview { r, ok := v.pending[url] if !ok { diff --git a/native/cmd/lich-native/pulls_review_logic.go b/native/cmd/lich-native/pulls_review_logic.go index cf9c3382..bc432415 100644 --- a/native/cmd/lich-native/pulls_review_logic.go +++ b/native/cmd/lich-native/pulls_review_logic.go @@ -104,6 +104,15 @@ func (r *pendingReview) remove(i int) { r.mu.Unlock() } +// edit rewords comment i, keeping where it points. +func (r *pendingReview) edit(i int, body string) { + r.mu.Lock() + if i >= 0 && i < len(r.comments) { + r.comments[i].Body = body + } + r.mu.Unlock() +} + func (r *pendingReview) clear() { r.mu.Lock() r.comments = nil diff --git a/native/cmd/lich-native/pulls_review_logic_test.go b/native/cmd/lich-native/pulls_review_logic_test.go index 4b0ec115..9400dc11 100644 --- a/native/cmd/lich-native/pulls_review_logic_test.go +++ b/native/cmd/lich-native/pulls_review_logic_test.go @@ -74,3 +74,14 @@ func TestSubmittedReviewDropsOnlyWhatWasSent(t *testing.T) { t.Fatalf("pending after the submit = %+v, want only the later comment", got) } } + +func TestPendingReviewEditsOneComment(t *testing.T) { + var r pendingReview + r.add("a.go", "3-5", "first") + r.add("b.go", "9", "second") + r.edit(0, "first, reworded\non two lines") + got := r.snapshot() + if len(got) != 2 || got[0].Body != "first, reworded\non two lines" || got[0].Path != "a.go" || got[0].StartLine != 3 || got[0].Line != 5 || got[1].Body != "second" { + t.Fatalf("after edit = %+v, want the first reworded in place, the second untouched", got) + } +} diff --git a/native/i18n/locales/en/diff.json b/native/i18n/locales/en/diff.json index 7f6a0fd4..7fe30c28 100644 --- a/native/i18n/locales/en/diff.json +++ b/native/i18n/locales/en/diff.json @@ -19,6 +19,10 @@ "send": "Send", "sendFailed": "Couldn't send the comments" }, + "commentEdit": { + "title": "Edit comment", + "save": "Save" + }, "diffBulk": { "collapseAll": "Collapse all files", "expandAll": "Expand all files" diff --git a/native/i18n/locales/en/pulls.json b/native/i18n/locales/en/pulls.json index 5e9bd0bb..1764df10 100644 --- a/native/i18n/locales/en/pulls.json +++ b/native/i18n/locales/en/pulls.json @@ -104,7 +104,9 @@ "outdated": "outdated", "pendingHead": "Pending review", "pendingNote": "Sent when you submit the review.", - "discard": "Discard" + "discard": "Discard", + "edit": "Edit", + "save": "Save" }, "pullsOverview": { "openedBy": "Opened by {author}", diff --git a/native/i18n/locales/es/diff.json b/native/i18n/locales/es/diff.json index 94d3f667..9505039f 100644 --- a/native/i18n/locales/es/diff.json +++ b/native/i18n/locales/es/diff.json @@ -19,6 +19,10 @@ "send": "Enviar", "sendFailed": "No se pudieron enviar los comentarios" }, + "commentEdit": { + "title": "Editar comentario", + "save": "Guardar" + }, "diffBulk": { "collapseAll": "Contraer todos los archivos", "expandAll": "Expandir todos los archivos" diff --git a/native/i18n/locales/es/pulls.json b/native/i18n/locales/es/pulls.json index e4f501e1..933519ae 100644 --- a/native/i18n/locales/es/pulls.json +++ b/native/i18n/locales/es/pulls.json @@ -104,7 +104,9 @@ "outdated": "desactualizado", "pendingHead": "Revisión pendiente", "pendingNote": "Se envía cuando envías la revisión.", - "discard": "Descartar" + "discard": "Descartar", + "edit": "Editar", + "save": "Guardar" }, "pullsOverview": { "openedBy": "Abierto por {author}", diff --git a/native/i18n/locales/ja/diff.json b/native/i18n/locales/ja/diff.json index 54165c03..6ac42e49 100644 --- a/native/i18n/locales/ja/diff.json +++ b/native/i18n/locales/ja/diff.json @@ -19,6 +19,10 @@ "send": "送信", "sendFailed": "コメントを送信できませんでした" }, + "commentEdit": { + "title": "コメントを編集", + "save": "保存" + }, "diffBulk": { "collapseAll": "すべてのファイルを折りたたむ", "expandAll": "すべてのファイルを展開" diff --git a/native/i18n/locales/ja/pulls.json b/native/i18n/locales/ja/pulls.json index 59d9a716..08d1b4ee 100644 --- a/native/i18n/locales/ja/pulls.json +++ b/native/i18n/locales/ja/pulls.json @@ -104,7 +104,9 @@ "outdated": "古い", "pendingHead": "保留中のレビュー", "pendingNote": "レビューを送信すると送られます。", - "discard": "破棄" + "discard": "破棄", + "edit": "編集", + "save": "保存" }, "pullsOverview": { "openedBy": "{author} が作成", diff --git a/native/i18n/locales/ko/diff.json b/native/i18n/locales/ko/diff.json index 1d792725..2dbec76a 100644 --- a/native/i18n/locales/ko/diff.json +++ b/native/i18n/locales/ko/diff.json @@ -18,6 +18,10 @@ "send": "보내기", "sendFailed": "댓글을 보내지 못했습니다" }, + "commentEdit": { + "title": "코멘트 편집", + "save": "저장" + }, "diffBulk": { "collapseAll": "모든 파일 접기", "expandAll": "모든 파일 펼치기" diff --git a/native/i18n/locales/ko/pulls.json b/native/i18n/locales/ko/pulls.json index 2474d5e0..a09f5895 100644 --- a/native/i18n/locales/ko/pulls.json +++ b/native/i18n/locales/ko/pulls.json @@ -102,7 +102,9 @@ "outdated": "오래됨", "pendingHead": "대기 중인 리뷰", "pendingNote": "리뷰를 제출할 때 보내집니다.", - "discard": "버리기" + "discard": "버리기", + "edit": "편집", + "save": "저장" }, "pullsOverview": { "openedBy": "{author}이(가) 엶", diff --git a/native/i18n/locales/pt-br/diff.json b/native/i18n/locales/pt-br/diff.json index 2020dca3..82427365 100644 --- a/native/i18n/locales/pt-br/diff.json +++ b/native/i18n/locales/pt-br/diff.json @@ -19,6 +19,10 @@ "send": "Enviar", "sendFailed": "Não foi possível enviar os comentários" }, + "commentEdit": { + "title": "Editar comentário", + "save": "Salvar" + }, "diffBulk": { "collapseAll": "Recolher todos os arquivos", "expandAll": "Expandir todos os arquivos" diff --git a/native/i18n/locales/pt-br/pulls.json b/native/i18n/locales/pt-br/pulls.json index f3164d61..60705f90 100644 --- a/native/i18n/locales/pt-br/pulls.json +++ b/native/i18n/locales/pt-br/pulls.json @@ -104,7 +104,9 @@ "outdated": "desatualizada", "pendingHead": "Revisão pendente", "pendingNote": "Enviada quando você enviar a revisão.", - "discard": "Descartar" + "discard": "Descartar", + "edit": "Editar", + "save": "Salvar" }, "pullsOverview": { "openedBy": "Aberto por {author}", diff --git a/native/i18n/locales/ru/diff.json b/native/i18n/locales/ru/diff.json index 5b8a5dc9..d7912551 100644 --- a/native/i18n/locales/ru/diff.json +++ b/native/i18n/locales/ru/diff.json @@ -21,6 +21,10 @@ "send": "Отправить", "sendFailed": "Не удалось отправить комментарии" }, + "commentEdit": { + "title": "Изменить комментарий", + "save": "Сохранить" + }, "diffBulk": { "collapseAll": "Свернуть все файлы", "expandAll": "Развернуть все файлы" diff --git a/native/i18n/locales/ru/pulls.json b/native/i18n/locales/ru/pulls.json index 729a4bbe..b9adadc9 100644 --- a/native/i18n/locales/ru/pulls.json +++ b/native/i18n/locales/ru/pulls.json @@ -108,7 +108,9 @@ "outdated": "устарело", "pendingHead": "Черновик ревью", "pendingNote": "Отправится вместе с ревью.", - "discard": "Отбросить" + "discard": "Отбросить", + "edit": "Изменить", + "save": "Сохранить" }, "pullsOverview": { "openedBy": "Открыл(а) {author}", diff --git a/native/i18n/locales/zh-cn/diff.json b/native/i18n/locales/zh-cn/diff.json index 30c29386..e2c8baef 100644 --- a/native/i18n/locales/zh-cn/diff.json +++ b/native/i18n/locales/zh-cn/diff.json @@ -19,6 +19,10 @@ "send": "发送", "sendFailed": "无法发送评论" }, + "commentEdit": { + "title": "编辑评论", + "save": "保存" + }, "diffBulk": { "collapseAll": "折叠所有文件", "expandAll": "展开所有文件" diff --git a/native/i18n/locales/zh-cn/pulls.json b/native/i18n/locales/zh-cn/pulls.json index 215cdd5a..e4c0b3c3 100644 --- a/native/i18n/locales/zh-cn/pulls.json +++ b/native/i18n/locales/zh-cn/pulls.json @@ -104,7 +104,9 @@ "outdated": "已过时", "pendingHead": "待提交的评审", "pendingNote": "提交评审时发送。", - "discard": "丢弃" + "discard": "丢弃", + "edit": "编辑", + "save": "保存" }, "pullsOverview": { "openedBy": "由 {author} 打开",