diff options
Diffstat (limited to 'lib/marker')
-rw-r--r-- | lib/marker/marker.go | 179 | ||||
-rw-r--r-- | lib/marker/marker_test.go | 139 |
2 files changed, 318 insertions, 0 deletions
diff --git a/lib/marker/marker.go b/lib/marker/marker.go new file mode 100644 index 00000000..1b89fc80 --- /dev/null +++ b/lib/marker/marker.go @@ -0,0 +1,179 @@ +package marker + +// TODO: fix headers for message that are nil + +// Marker provides the interface for the marking behavior of messages +type Marker interface { + Mark(uint32) + Unmark(uint32) + ToggleMark(uint32) + Remark() + Marked() []uint32 + IsMarked(uint32) bool + ToggleVisualMark() + UpdateVisualMark() + ClearVisualMark() +} + +// UIDProvider provides the underlying uids and the selected message index +type UIDProvider interface { + Uids() []uint32 + SelectedIndex() int +} + +type controller struct { + uidProvider UIDProvider + marked map[uint32]struct{} + lastMarked map[uint32]struct{} + visualStartUID uint32 + visualMarkMode bool +} + +// New returns a new Marker +func New(up UIDProvider) Marker { + return &controller{ + uidProvider: up, + marked: make(map[uint32]struct{}), + lastMarked: make(map[uint32]struct{}), + } +} + +// Mark markes the uid as marked +func (mc *controller) Mark(uid uint32) { + if mc.visualMarkMode { + // visual mode has override, bogus input from user + return + } + mc.marked[uid] = struct{}{} +} + +// Unmark unmarks the uid +func (mc *controller) Unmark(uid uint32) { + if mc.visualMarkMode { + // user probably wanted to clear the visual marking + mc.ClearVisualMark() + return + } + delete(mc.marked, uid) +} + +// Remark restores the previous marks +func (mc *controller) Remark() { + mc.marked = mc.lastMarked +} + +// ToggleMark toggles the marked state for the given uid +func (mc *controller) ToggleMark(uid uint32) { + if mc.visualMarkMode { + // visual mode has override, bogus input from user + return + } + if mc.IsMarked(uid) { + mc.Unmark(uid) + } else { + mc.Mark(uid) + } +} + +// resetMark removes the marking from all messages +func (mc *controller) resetMark() { + mc.lastMarked = mc.marked + mc.marked = make(map[uint32]struct{}) +} + +// removeStaleUID removes uids that are no longer presents in the UIDProvider +func (mc *controller) removeStaleUID() { + for mark := range mc.marked { + present := false + for _, uid := range mc.uidProvider.Uids() { + if mark == uid { + present = true + break + } + } + if !present { + delete(mc.marked, mark) + } + } +} + +// IsMarked checks whether the given uid has been marked +func (mc *controller) IsMarked(uid uint32) bool { + _, marked := mc.marked[uid] + return marked +} + +// Marked returns the uids of all marked messages +func (mc *controller) Marked() []uint32 { + mc.removeStaleUID() + marked := make([]uint32, len(mc.marked)) + i := 0 + for uid := range mc.marked { + marked[i] = uid + i++ + } + return marked +} + +// ToggleVisualMark enters or leaves the visual marking mode +func (mc *controller) ToggleVisualMark() { + mc.visualMarkMode = !mc.visualMarkMode + if mc.visualMarkMode { + // just entered visual mode, reset whatever marking was already done + mc.resetMark() + uids := mc.uidProvider.Uids() + if idx := mc.uidProvider.SelectedIndex(); idx >= 0 && idx < len(uids) { + mc.visualStartUID = uids[idx] + mc.marked[mc.visualStartUID] = struct{}{} + } + } +} + +// ClearVisualMark leaves the visual marking mode and resets any marking +func (mc *controller) ClearVisualMark() { + mc.resetMark() + mc.visualMarkMode = false + mc.visualStartUID = 0 +} + +// UpdateVisualMark updates the index with the currently selected message +func (mc *controller) UpdateVisualMark() { + if !mc.visualMarkMode { + // nothing to do + return + } + startIdx := mc.visualStartIdx() + if startIdx < 0 { + // something deleted the startuid, abort the marking process + mc.ClearVisualMark() + return + } + + selectedIdx := mc.uidProvider.SelectedIndex() + if selectedIdx < 0 { + return + } + + uids := mc.uidProvider.Uids() + + var visUids []uint32 + if selectedIdx > startIdx { + visUids = uids[startIdx : selectedIdx+1] + } else { + visUids = uids[selectedIdx : startIdx+1] + } + mc.resetMark() + for _, uid := range visUids { + mc.marked[uid] = struct{}{} + } +} + +// returns the index of needle in haystack or -1 if not found +func (mc *controller) visualStartIdx() int { + for idx, u := range mc.uidProvider.Uids() { + if u == mc.visualStartUID { + return idx + } + } + return -1 +} diff --git a/lib/marker/marker_test.go b/lib/marker/marker_test.go new file mode 100644 index 00000000..1611623e --- /dev/null +++ b/lib/marker/marker_test.go @@ -0,0 +1,139 @@ +package marker_test + +import ( + "testing" + + "git.sr.ht/~rjarry/aerc/lib/marker" +) + +// mockUidProvider implements the UidProvider interface and mocks the message +// store for testing +type mockUidProvider struct { + uids []uint32 + idx int +} + +func (mock *mockUidProvider) Uids() []uint32 { + return mock.uids +} + +func (mock *mockUidProvider) SelectedIndex() int { + return mock.idx +} + +func createMarker() (marker.Marker, *mockUidProvider) { + uidProvider := &mockUidProvider{ + uids: []uint32{1, 2, 3, 4}, + idx: 1, + } + m := marker.New(uidProvider) + return m, uidProvider +} + +func TestMarker_MarkUnmark(t *testing.T) { + m, _ := createMarker() + uid := uint32(4) + + m.Mark(uid) + if !m.IsMarked(uid) { + t.Errorf("Marking failed") + } + + m.Unmark(uid) + if m.IsMarked(uid) { + t.Errorf("Unmarking failed") + } +} + +func TestMarker_ToggleMark(t *testing.T) { + m, _ := createMarker() + uid := uint32(4) + + if m.IsMarked(uid) { + t.Errorf("ToggleMark: uid should not be marked") + } + + m.ToggleMark(uid) + if !m.IsMarked(uid) { + t.Errorf("ToggleMark: uid should be marked") + } + + m.ToggleMark(uid) + if m.IsMarked(uid) { + t.Errorf("ToggleMark: uid should not be marked") + } +} + +func TestMarker_Marked(t *testing.T) { + m, _ := createMarker() + expected := map[uint32]struct{}{ + uint32(1): {}, + uint32(4): {}, + } + for uid := range expected { + m.Mark(uid) + } + + got := m.Marked() + if len(expected) != len(got) { + t.Errorf("Marked: expected len of %d but got %d", len(expected), len(got)) + } + + for _, uid := range got { + if _, ok := expected[uid]; !ok { + t.Errorf("Marked: received uid %d as marked but it should not be", uid) + } + } +} + +func TestMarker_VisualMode(t *testing.T) { + m, up := createMarker() + + // activate visual mode + m.ToggleVisualMark() + + // marking should now fail silently because we're in visual mode + m.Mark(1) + if m.IsMarked(1) { + t.Errorf("marking in visual mode should not work") + } + + // move selection index to last item + up.idx = len(up.uids) - 1 + m.UpdateVisualMark() + expectedMarked := []uint32{2, 3, 4} + + for _, uidMarked := range expectedMarked { + if !m.IsMarked(uidMarked) { + t.Logf("expected: %#v, got: %#v", expectedMarked, m.Marked()) + t.Errorf("updatevisual: uid %v should be marked in visual mode", uidMarked) + } + } + + // clear all + m.ClearVisualMark() + if len(m.Marked()) > 0 { + t.Errorf("no uids should be marked after clearing visual mark") + } + + // test remark + m.Remark() + for _, uidMarked := range expectedMarked { + if !m.IsMarked(uidMarked) { + t.Errorf("remark: uid %v should be marked in visual mode", uidMarked) + } + } +} + +func TestMarker_MarkOutOfBound(t *testing.T) { + m, _ := createMarker() + + outOfBoundUid := uint32(100) + + m.Mark(outOfBoundUid) + for _, markedUid := range m.Marked() { + if markedUid == outOfBoundUid { + t.Errorf("out-of-bound uid should not be marked") + } + } +} |