Merge pull request #60 from adamslc/commentedit

Michael Muré created

termui:  add the ability to edit comments

Change summary

commands/comment_add.go |  2 +-
input/input.go          |  7 ++++---
termui/show_bug.go      | 39 ++++++++++++++++++++++++++++++++++++---
termui/termui.go        | 38 +++++++++++++++++++++++++++++++++++++-
4 files changed, 78 insertions(+), 8 deletions(-)

Detailed changes

commands/comment_add.go 🔗

@@ -29,7 +29,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
 	}
 
 	if commentAddMessage == "" {
-		commentAddMessage, err = input.BugCommentEditorInput(backend)
+		commentAddMessage, err = input.BugCommentEditorInput(backend, "")
 		if err == input.ErrEmptyMessage {
 			fmt.Println("Empty message, aborting.")
 			return nil

input/input.go 🔗

@@ -78,7 +78,7 @@ func BugCreateEditorInput(repo repository.RepoCommon, preTitle string, preMessag
 	return title, message, nil
 }
 
-const bugCommentTemplate = `
+const bugCommentTemplate = `%s
 
 # Please enter the comment message. Lines starting with '#' will be ignored,
 # and an empty message aborts the operation.
@@ -86,8 +86,9 @@ const bugCommentTemplate = `
 
 // BugCommentEditorInput will open the default editor in the terminal with a
 // template for the user to fill. The file is then processed to extract a comment.
-func BugCommentEditorInput(repo repository.RepoCommon) (string, error) {
-	raw, err := launchEditorWithTemplate(repo, messageFilename, bugCommentTemplate)
+func BugCommentEditorInput(repo repository.RepoCommon, preMessage string) (string, error) {
+	template := fmt.Sprintf(bugCommentTemplate, preMessage)
+	raw, err := launchEditorWithTemplate(repo, messageFilename, template)
 
 	if err != nil {
 		return "", err

termui/show_bug.go 🔗

@@ -9,6 +9,7 @@ import (
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/jroimartin/gocui"
 )
 
@@ -99,7 +100,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
 	if sb.isOnSide {
 		fmt.Fprint(v, "[a] Add label [r] Remove label")
 	} else {
-		fmt.Fprint(v, "[o] Toggle open/close [c] Comment [t] Change title")
+		fmt.Fprint(v, "[o] Toggle open/close [e] Edit [c] Comment [t] Change title")
 	}
 
 	_, err = g.SetViewOnTop(showBugInstructionView)
@@ -183,6 +184,12 @@ func (sb *showBug) keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	// Edit
+	if err := g.SetKeybinding(showBugView, 'e', gocui.ModNone,
+		sb.edit); err != nil {
+		return err
+	}
+
 	// Labels
 	if err := g.SetKeybinding(showBugView, 'a', gocui.ModNone,
 		sb.addLabel); err != nil {
@@ -240,8 +247,8 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 	fmt.Fprint(v, bugHeader)
 	y0 += lines + 1
 
-	for i, op := range snap.Timeline {
-		viewName := fmt.Sprintf("op%d", i)
+	for _, op := range snap.Timeline {
+		viewName := op.Hash().String()
 
 		// TODO: me might skip the rendering of blocks that are outside of the view
 		// but to do that we need to rework how sb.mainSelectableView is maintained
@@ -620,6 +627,32 @@ func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error {
 	}
 }
 
+func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
+	if sb.isOnSide {
+		ui.msgPopup.Activate(msgPopupErrorTitle, "Selected field is not editable.")
+		return nil
+	}
+
+	snap := sb.bug.Snapshot()
+
+	op, err := snap.SearchTimelineItem(git.Hash(sb.selected))
+	if err != nil {
+		return err
+	}
+
+	switch op.(type) {
+	case *bug.AddCommentTimelineItem:
+		message := op.(*bug.AddCommentTimelineItem).Message
+		return editCommentWithEditor(sb.bug, op.Hash(), message)
+	case *bug.CreateTimelineItem:
+		preMessage := op.(*bug.CreateTimelineItem).Message
+		return editCommentWithEditor(sb.bug, op.Hash(), preMessage)
+	}
+
+	ui.msgPopup.Activate(msgPopupErrorTitle, "Selected field is not editable.")
+	return nil
+}
+
 func (sb *showBug) addLabel(g *gocui.Gui, v *gocui.View) error {
 	c := ui.inputPopup.Activate("Add labels")
 

termui/termui.go 🔗

@@ -4,6 +4,7 @@ package termui
 import (
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/jroimartin/gocui"
 	"github.com/pkg/errors"
 )
@@ -210,7 +211,7 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	ui.g.Close()
 	ui.g = nil
 
-	message, err := input.BugCommentEditorInput(ui.cache)
+	message, err := input.BugCommentEditorInput(ui.cache, "")
 
 	if err != nil && err != input.ErrEmptyMessage {
 		return err
@@ -230,6 +231,41 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	return errTerminateMainloop
 }
 
+func editCommentWithEditor(bug *cache.BugCache, target git.Hash, preMessage string) error {
+	// This is somewhat hacky.
+	// As there is no way to pause gocui, run the editor and restart gocui,
+	// we have to stop it entirely and start a new one later.
+	//
+	// - an error channel is used to route the returned error of this new
+	// 		instance into the original launch function
+	// - a custom error (errTerminateMainloop) is used to terminate the original
+	//		instance's mainLoop. This error is then filtered.
+
+	ui.g.Close()
+	ui.g = nil
+
+	message, err := input.BugCommentEditorInput(ui.cache, preMessage)
+	if err != nil && err != input.ErrEmptyMessage {
+		return err
+	}
+
+	if err == input.ErrEmptyMessage {
+		// TODO: Allow comments to be deleted?
+		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
+	} else if message == preMessage {
+		ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.")
+	} else {
+		err := bug.EditComment(target, message)
+		if err != nil {
+			return err
+		}
+	}
+
+	initGui(nil)
+
+	return errTerminateMainloop
+}
+
 func setTitleWithEditor(bug *cache.BugCache) error {
 	// This is somewhat hacky.
 	// As there is no way to pause gocui, run the editor and restart gocui,