Merge branch 'labeledit'

Michael Muré created

Change summary

termui/label_select.go | 304 ++++++++++++++++++++++++++++++++++++++++++++
termui/show_bug.go     |  86 +-----------
termui/termui.go       |  26 ++-
3 files changed, 329 insertions(+), 87 deletions(-)

Detailed changes

termui/label_select.go 🔗

@@ -0,0 +1,304 @@
+package termui
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/jroimartin/gocui"
+)
+
+const labelSelectView = "labelSelectView"
+const labelSelectInstructionsView = "labelSelectInstructionsView"
+
+type labelSelect struct {
+	cache       *cache.RepoCache
+	bug         *cache.BugCache
+	labels      []bug.Label
+	labelSelect []bool
+	selected    int
+	scroll      int
+	childViews  []string
+}
+
+func newLabelSelect() *labelSelect {
+	return &labelSelect{}
+}
+
+func (ls *labelSelect) SetBug(cache *cache.RepoCache, bug *cache.BugCache) {
+	ls.cache = cache
+	ls.bug = bug
+	ls.labels = cache.ValidLabels()
+
+	// Find which labels are currently applied to the bug
+	bugLabels := bug.Snapshot().Labels
+	labelSelect := make([]bool, len(ls.labels))
+	for i, label := range ls.labels {
+		for _, bugLabel := range bugLabels {
+			if label == bugLabel {
+				labelSelect[i] = true
+				break
+			}
+		}
+	}
+
+	ls.labelSelect = labelSelect
+	if len(labelSelect) > 0 {
+		ls.selected = 0
+	} else {
+		ls.selected = -1
+	}
+
+	ls.scroll = 0
+}
+
+func (ls *labelSelect) keybindings(g *gocui.Gui) error {
+	// Abort
+	if err := g.SetKeybinding(labelSelectView, gocui.KeyEsc, gocui.ModNone, ls.abort); err != nil {
+		return err
+	}
+	// Save and return
+	if err := g.SetKeybinding(labelSelectView, 'q', gocui.ModNone, ls.saveAndReturn); err != nil {
+		return err
+	}
+	// Up
+	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowUp, gocui.ModNone, ls.selectPrevious); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(labelSelectView, 'k', gocui.ModNone, ls.selectPrevious); err != nil {
+		return err
+	}
+	// Down
+	if err := g.SetKeybinding(labelSelectView, gocui.KeyArrowDown, gocui.ModNone, ls.selectNext); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(labelSelectView, 'j', gocui.ModNone, ls.selectNext); err != nil {
+		return err
+	}
+	// Select
+	if err := g.SetKeybinding(labelSelectView, gocui.KeySpace, gocui.ModNone, ls.selectItem); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(labelSelectView, 'x', gocui.ModNone, ls.selectItem); err != nil {
+		return err
+	}
+	if err := g.SetKeybinding(labelSelectView, gocui.KeyEnter, gocui.ModNone, ls.selectItem); err != nil {
+		return err
+	}
+	// Add
+	if err := g.SetKeybinding(labelSelectView, 'a', gocui.ModNone, ls.addItem); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (ls *labelSelect) layout(g *gocui.Gui) error {
+	maxX, maxY := g.Size()
+	ls.childViews = nil
+
+	width := 5
+	for _, label := range ls.labels {
+		width = maxInt(width, len(label))
+	}
+	width += 10
+	x0 := 1
+	y0 := 0 - ls.scroll
+
+	v, err := g.SetView(labelSelectView, x0, 0, x0+width, maxY-2)
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+
+		v.Frame = false
+	}
+
+	for i, label := range ls.labels {
+		viewname := fmt.Sprintf("view%d", i)
+		v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2)
+		if err != nil && err != gocui.ErrUnknownView {
+			return err
+		}
+		ls.childViews = append(ls.childViews, viewname)
+		v.Frame = i == ls.selected
+		v.Clear()
+		selectBox := " [ ] "
+		if ls.labelSelect[i] {
+			selectBox = " [x] "
+		}
+		fmt.Fprint(v, selectBox, label)
+		y0 += 2
+	}
+
+	v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY)
+	ls.childViews = append(ls.childViews, labelSelectInstructionsView)
+	if err != nil {
+		if err != gocui.ErrUnknownView {
+			return err
+		}
+		v.Frame = false
+		v.BgColor = gocui.ColorBlue
+	}
+	v.Clear()
+	fmt.Fprint(v, "[q] Save and close [↓↑,jk] Nav [a] Add item")
+	if _, err = g.SetViewOnTop(labelSelectInstructionsView); err != nil {
+		return err
+	}
+	if _, err := g.SetCurrentView(labelSelectView); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (ls *labelSelect) disable(g *gocui.Gui) error {
+	for _, view := range ls.childViews {
+		if err := g.DeleteView(view); err != nil && err != gocui.ErrUnknownView {
+			return err
+		}
+	}
+	return nil
+}
+
+func (ls *labelSelect) focusView(g *gocui.Gui) error {
+	if ls.selected < 0 {
+		return nil
+	}
+
+	_, lsy0, _, lsy1, err := g.ViewPosition(labelSelectView)
+	if err != nil {
+		return err
+	}
+
+	_, vy0, _, vy1, err := g.ViewPosition(fmt.Sprintf("view%d", ls.selected))
+	if err != nil {
+		return err
+	}
+
+	// Below bottom of frame
+	if vy1 > lsy1 {
+		ls.scroll += vy1 - lsy1
+		return nil
+	}
+
+	// Above top of frame
+	if vy0 < lsy0 {
+		ls.scroll -= lsy0 - vy0
+	}
+
+	return nil
+}
+
+func (ls *labelSelect) selectPrevious(g *gocui.Gui, v *gocui.View) error {
+	if ls.selected < 0 {
+		return nil
+	}
+
+	ls.selected = maxInt(0, ls.selected-1)
+	return ls.focusView(g)
+}
+
+func (ls *labelSelect) selectNext(g *gocui.Gui, v *gocui.View) error {
+	if ls.selected < 0 {
+		return nil
+	}
+
+	ls.selected = minInt(len(ls.labels)-1, ls.selected+1)
+	return ls.focusView(g)
+}
+
+func (ls *labelSelect) selectItem(g *gocui.Gui, v *gocui.View) error {
+	if ls.selected < 0 {
+		return nil
+	}
+
+	ls.labelSelect[ls.selected] = !ls.labelSelect[ls.selected]
+	return nil
+}
+
+func (ls *labelSelect) addItem(g *gocui.Gui, v *gocui.View) error {
+	c := ui.inputPopup.Activate("Add a new label")
+
+	go func() {
+		input := <-c
+
+		// Standardize label format
+		input = strings.TrimSuffix(input, "\n")
+		input = strings.Replace(input, " ", "-", -1)
+
+		// Check if label already exists
+		for i, label := range ls.labels {
+			if input == label.String() {
+				ls.labelSelect[i] = true
+				ls.selected = i
+
+				g.Update(func(gui *gocui.Gui) error {
+					return ls.focusView(g)
+				})
+
+				return
+			}
+		}
+
+		// Add new label, make it selected, and focus
+		ls.labels = append(ls.labels, bug.Label(input))
+		ls.labelSelect = append(ls.labelSelect, true)
+		ls.selected = len(ls.labels) - 1
+
+		g.Update(func(g *gocui.Gui) error {
+			return nil
+		})
+	}()
+
+	return nil
+}
+
+func (ls *labelSelect) abort(g *gocui.Gui, v *gocui.View) error {
+	return ui.activateWindow(ui.showBug)
+}
+
+func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
+	bugLabels := ls.bug.Snapshot().Labels
+	var selectedLabels []bug.Label
+	for i, label := range ls.labels {
+		if ls.labelSelect[i] {
+			selectedLabels = append(selectedLabels, label)
+		}
+	}
+
+	// Find the new and removed labels. This could be implemented more efficiently...
+	var newLabels []string
+	var rmLabels []string
+
+	for _, selectedLabel := range selectedLabels {
+		found := false
+		for _, bugLabel := range bugLabels {
+			if selectedLabel == bugLabel {
+				found = true
+			}
+		}
+
+		if !found {
+			newLabels = append(newLabels, string(selectedLabel))
+		}
+	}
+
+	for _, bugLabel := range bugLabels {
+		found := false
+		for _, selectedLabel := range selectedLabels {
+			if bugLabel == selectedLabel {
+				found = true
+			}
+		}
+
+		if !found {
+			rmLabels = append(rmLabels, string(bugLabel))
+		}
+	}
+
+	if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
+		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
+	}
+
+	return ui.activateWindow(ui.showBug)
+}

termui/show_bug.go 🔗

@@ -95,13 +95,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
 	}
 
 	v.Clear()
-	fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation ")
-
-	if sb.isOnSide {
-		fmt.Fprint(v, "[a] Add label [r] Remove label")
-	} else {
-		fmt.Fprint(v, "[o] Toggle open/close [e] Edit [c] Comment [t] Change title")
-	}
+	fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation [o] Toggle open/close [e] Edit [c] Comment [t] Change title")
 
 	_, err = g.SetViewOnTop(showBugInstructionView)
 	if err != nil {
@@ -190,16 +184,6 @@ func (sb *showBug) keybindings(g *gocui.Gui) error {
 		return err
 	}
 
-	// Labels
-	if err := g.SetKeybinding(showBugView, 'a', gocui.ModNone,
-		sb.addLabel); err != nil {
-		return err
-	}
-	if err := g.SetKeybinding(showBugView, 'r', gocui.ModNone,
-		sb.removeLabel); err != nil {
-		return err
-	}
-
 	return nil
 }
 
@@ -628,13 +612,12 @@ func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error {
 }
 
 func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
+	snap := sb.bug.Snapshot()
+
 	if sb.isOnSide {
-		ui.msgPopup.Activate(msgPopupErrorTitle, "Selected field is not editable.")
-		return nil
+		return sb.editLabels(g, snap)
 	}
 
-	snap := sb.bug.Snapshot()
-
 	op, err := snap.SearchTimelineItem(git.Hash(sb.selected))
 	if err != nil {
 		return err
@@ -647,66 +630,15 @@ func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
 	case *bug.CreateTimelineItem:
 		preMessage := op.(*bug.CreateTimelineItem).Message
 		return editCommentWithEditor(sb.bug, op.Hash(), preMessage)
+	case *bug.LabelChangeTimelineItem:
+		return sb.editLabels(g, snap)
 	}
 
 	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")
-
-	go func() {
-		input := <-c
-
-		labels := strings.FieldsFunc(input, func(r rune) bool {
-			return r == ' ' || r == ','
-		})
-
-		_, err := sb.bug.ChangeLabels(trimLabels(labels), nil)
-		if err != nil {
-			ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
-		}
-
-		g.Update(func(gui *gocui.Gui) error {
-			return nil
-		})
-	}()
-
-	return nil
-}
-
-func (sb *showBug) removeLabel(g *gocui.Gui, v *gocui.View) error {
-	c := ui.inputPopup.Activate("Remove labels")
-
-	go func() {
-		input := <-c
-
-		labels := strings.FieldsFunc(input, func(r rune) bool {
-			return r == ' ' || r == ','
-		})
-
-		_, err := sb.bug.ChangeLabels(nil, trimLabels(labels))
-		if err != nil {
-			ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
-		}
-
-		g.Update(func(gui *gocui.Gui) error {
-			return nil
-		})
-	}()
-
-	return nil
-}
-
-func trimLabels(labels []string) []string {
-	var result []string
-
-	for _, label := range labels {
-		trimmed := strings.TrimSpace(label)
-		if len(trimmed) > 0 {
-			result = append(result, trimmed)
-		}
-	}
-	return result
+func (sb *showBug) editLabels(g *gocui.Gui, snap *bug.Snapshot) error {
+	ui.labelSelect.SetBug(sb.cache, sb.bug)
+	return ui.activateWindow(ui.labelSelect)
 }

termui/termui.go 🔗

@@ -18,10 +18,11 @@ type termUI struct {
 
 	activeWindow window
 
-	bugTable   *bugTable
-	showBug    *showBug
-	msgPopup   *msgPopup
-	inputPopup *inputPopup
+	bugTable    *bugTable
+	showBug     *showBug
+	labelSelect *labelSelect
+	msgPopup    *msgPopup
+	inputPopup  *inputPopup
 }
 
 func (tui *termUI) activateWindow(window window) error {
@@ -45,12 +46,13 @@ type window interface {
 // Run will launch the termUI in the terminal
 func Run(cache *cache.RepoCache) error {
 	ui = &termUI{
-		gError:     make(chan error, 1),
-		cache:      cache,
-		bugTable:   newBugTable(cache),
-		showBug:    newShowBug(cache),
-		msgPopup:   newMsgPopup(),
-		inputPopup: newInputPopup(),
+		gError:      make(chan error, 1),
+		cache:       cache,
+		bugTable:    newBugTable(cache),
+		showBug:     newShowBug(cache),
+		labelSelect: newLabelSelect(),
+		msgPopup:    newMsgPopup(),
+		inputPopup:  newInputPopup(),
 	}
 
 	ui.activeWindow = ui.bugTable
@@ -143,6 +145,10 @@ func keybindings(g *gocui.Gui) error {
 		return err
 	}
 
+	if err := ui.labelSelect.keybindings(g); err != nil {
+		return err
+	}
+
 	if err := ui.msgPopup.keybindings(g); err != nil {
 		return err
 	}