From 4c63c62bfa36f70f91c8a0e492b4b7279d8c8562 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Fri, 27 Nov 2020 21:52:51 -0500 Subject: [PATCH] Replace bit-based document type implementation with a set-based one --- main.go | 5 +-- ui/doctypes.go | 78 +++++++++++++++++++++++++++++++++++++++++++++ ui/doctypes_test.go | 48 ++++++++++++++++++++++++++++ ui/markdown.go | 15 ++------- ui/pager.go | 12 +++---- ui/stash.go | 74 +++++++++++++++++++++--------------------- ui/stashitem.go | 6 ++-- ui/ui.go | 20 ++++++------ 8 files changed, 189 insertions(+), 69 deletions(-) create mode 100644 ui/doctypes.go create mode 100644 ui/doctypes_test.go diff --git a/main.go b/main.go index 8617487..aaf5df2 100644 --- a/main.go +++ b/main.go @@ -289,14 +289,15 @@ func runTUI(stashedOnly bool) error { defer f.Close() } + cfg.DocumentTypes = ui.NewDocTypeSet() cfg.ShowAllFiles = showAllFiles cfg.GlamourMaxWidth = width cfg.GlamourStyle = style if stashedOnly { - cfg.DocumentTypes = ui.StashedDocument | ui.NewsDocument + cfg.DocumentTypes.Add(ui.StashedDoc, ui.NewsDoc) } else if localOnly { - cfg.DocumentTypes = ui.LocalDocument + cfg.DocumentTypes.Add(ui.LocalDoc) } // Run Bubble Tea program diff --git a/ui/doctypes.go b/ui/doctypes.go new file mode 100644 index 0000000..ac35476 --- /dev/null +++ b/ui/doctypes.go @@ -0,0 +1,78 @@ +package ui + +// DocType represents a type of markdown document. +type DocType int + +// Available document types. +const ( + LocalDoc DocType = iota + StashedDoc + ConvertedDoc + NewsDoc +) + +// DocTypeSet is a set (in the mathematic sense) of document types. +type DocTypeSet map[DocType]struct{} + +// NewDocTypeSet returns a set of document types. +func NewDocTypeSet(t ...DocType) DocTypeSet { + d := DocTypeSet(make(map[DocType]struct{})) + if len(t) > 0 { + d.Add(t...) + } + return d +} + +// Add adds a document type of the set. +func (d *DocTypeSet) Add(t ...DocType) int { + for _, v := range t { + (*d)[v] = struct{}{} + } + return len(*d) +} + +// Had returns whether or not the set contains the given DocTypes. +func (d DocTypeSet) Contains(m ...DocType) bool { + matches := 0 + for _, t := range m { + if _, found := d[t]; found { + matches++ + } + } + return matches == len(m) +} + +// Difference return a DocumentType set that does not contain the given types. +func (d DocTypeSet) Difference(t ...DocType) DocTypeSet { + c := copyDocumentTypes(d) + for k := range c { + for _, docType := range t { + if k == docType { + delete(c, k) + break + } + } + } + return c +} + +// Equals returns whether or not the two sets are equal. +func (d DocTypeSet) Equals(other DocTypeSet) bool { + return d.Contains(other.asSlice()...) && len(d) == len(other) +} + +func (d DocTypeSet) asSlice() (agg []DocType) { + for k := range d { + agg = append(agg, k) + } + return +} + +// Return a copy of the given DocumentTypes map. +func copyDocumentTypes(d DocTypeSet) DocTypeSet { + c := make(map[DocType]struct{}) + for k, v := range d { + c[k] = v + } + return c +} diff --git a/ui/doctypes_test.go b/ui/doctypes_test.go new file mode 100644 index 0000000..e882b14 --- /dev/null +++ b/ui/doctypes_test.go @@ -0,0 +1,48 @@ +package ui + +import ( + "reflect" + "testing" +) + +func TestDocTypeContains(t *testing.T) { + d := NewDocTypeSet(LocalDoc) + + if !d.Contains(LocalDoc) { + t.Error("Contains reported it doesn't contain a value which it absolutely does contain") + } + + if d.Contains(NewsDoc) { + t.Error("Contains reported the set contains a value it certainly does not") + } +} + +func TestDocTypeDifference(t *testing.T) { + original := NewDocTypeSet(LocalDoc, StashedDoc, ConvertedDoc, NewsDoc) + difference := original.Difference(LocalDoc, NewsDoc) + expected := NewDocTypeSet(StashedDoc, ConvertedDoc) + + // Make sure the difference operation worked + if !reflect.DeepEqual(difference, expected) { + t.Errorf("difference returned %+v; expected %+v", difference, expected) + } + + // Make sure original set was not mutated + if reflect.DeepEqual(original, difference) { + t.Errorf("original set was mutated when it should not have been") + } +} + +func TestDocTypeEquality(t *testing.T) { + a := NewDocTypeSet(LocalDoc, StashedDoc) + b := NewDocTypeSet(LocalDoc, StashedDoc) + c := NewDocTypeSet(LocalDoc) + + if !a.Equals(b) { + t.Errorf("Equality test failed for %+v and %+v; expected true, got false", a, b) + } + + if a.Equals(c) { + t.Errorf("Equality test failed for %+v and %+v; expected false, got true", a, c) + } +} diff --git a/ui/markdown.go b/ui/markdown.go index 91679b4..746523b 100644 --- a/ui/markdown.go +++ b/ui/markdown.go @@ -13,18 +13,9 @@ import ( "golang.org/x/text/unicode/norm" ) -type DocumentType byte - -const ( - LocalDocument DocumentType = 1 << iota - StashedDocument - ConvertedDocument - NewsDocument -) - // markdown wraps charm.Markdown. type markdown struct { - markdownType DocumentType + markdownType DocType // Full path of a local markdown file. Only relevant to local documents and // those that have been stashed in this session. @@ -53,7 +44,7 @@ func (m *markdown) buildFilterValue() { // sortAsLocal returns whether or not this markdown should be sorted as though // it's a local markdown document. func (m markdown) sortAsLocal() bool { - return m.markdownType == LocalDocument || m.markdownType == ConvertedDocument + return m.markdownType == LocalDoc || m.markdownType == ConvertedDoc } // Sort documents with local files first, then by date. @@ -108,7 +99,7 @@ func isMn(r rune) bool { // wrapMarkdowns wraps a *charm.Markdown with a *markdown in order to add some // extra metadata. -func wrapMarkdowns(t DocumentType, md []*charm.Markdown) (m []*markdown) { +func wrapMarkdowns(t DocType, md []*charm.Markdown) (m []*markdown) { for _, v := range md { m = append(m, &markdown{ markdownType: t, diff --git a/ui/pager.go b/ui/pager.go index cb16a59..33ea846 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -215,8 +215,8 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { cmds = append(cmds, viewport.Sync(m.viewport)) } case "m": - isStashed := m.currentDocument.markdownType == StashedDocument || - m.currentDocument.markdownType == ConvertedDocument + isStashed := m.currentDocument.markdownType == StashedDoc || + m.currentDocument.markdownType == ConvertedDoc // Users can only set the note on user-stashed markdown if !isStashed { @@ -244,7 +244,7 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { } // Stash a local document - if m.state != pagerStateStashing && m.currentDocument.markdownType == LocalDocument { + if m.state != pagerStateStashing && m.currentDocument.markdownType == LocalDoc { m.state = pagerStateStashing m.spinner.Start() cmds = append( @@ -348,7 +348,7 @@ func (m pagerModel) statusBarView(b *strings.Builder) { percentToStringMagnitude float64 = 100.0 ) var ( - isStashed bool = m.currentDocument.markdownType == StashedDocument || m.currentDocument.markdownType == ConvertedDocument + isStashed bool = m.currentDocument.markdownType == StashedDoc || m.currentDocument.markdownType == ConvertedDoc showStatusMessage bool = m.state == pagerStateStatusMessage ) @@ -440,7 +440,7 @@ func (m pagerModel) setNoteView(b *strings.Builder) { func (m pagerModel) helpView() (s string) { memoOrStash := "m set memo" - if m.general.authStatus == authOK && m.currentDocument.markdownType == LocalDocument { + if m.general.authStatus == authOK && m.currentDocument.markdownType == LocalDoc { memoOrStash = "s stash this document" } @@ -453,7 +453,7 @@ func (m pagerModel) helpView() (s string) { "q quit", } - if m.currentDocument.markdownType == NewsDocument { + if m.currentDocument.markdownType == NewsDoc { deleteFromStringSlice(col1, 3) } diff --git a/ui/stash.go b/ui/stash.go index 8e77caa..27b7f43 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -77,9 +77,11 @@ type stashModel struct { spinner spinner.Model noteInput textinput.Model filterInput textinput.Model - stashFullyLoaded bool // have we loaded all available stashed documents from the server? - loadingFromNetwork bool // are we currently loading something from the network? - loaded DocumentType // load status for news, stash and local files loading; we find out exactly with bitmasking + stashFullyLoaded bool // have we loaded all available stashed documents from the server? + loadingFromNetwork bool // are we currently loading something from the network? + + // Tracks what exactly is loaded between the stash, news and local files + loaded DocTypeSet // The master set of markdown documents we're working with. markdowns []*markdown @@ -114,16 +116,15 @@ type stashModel struct { } func (m stashModel) localOnly() bool { - return m.general.cfg.DocumentTypes^LocalDocument == 0 + return m.general.cfg.DocumentTypes.Equals(NewDocTypeSet(LocalDoc)) } func (m stashModel) stashedOnly() bool { - return m.general.cfg.DocumentTypes&LocalDocument == 0 + return m.general.cfg.DocumentTypes.Equals(NewDocTypeSet(StashedDoc)) } func (m stashModel) loadingDone() bool { - // Do the types loaded match the types we want to have? - return m.loaded == m.general.cfg.DocumentTypes + return m.loaded.Equals(m.general.cfg.DocumentTypes) } // Returns whether or not we're online. That is, when "local-only" mode is @@ -251,7 +252,7 @@ func (m *stashModel) replaceLocalMarkdown(localPath string, newMarkdown *markdow } // Return the number of markdown documents of a given type. -func (m stashModel) countMarkdowns(t DocumentType) (found int) { +func (m stashModel) countMarkdowns(t DocType) (found int) { mds := m.getVisibleMarkdowns() if len(mds) == 0 { return @@ -265,7 +266,7 @@ func (m stashModel) countMarkdowns(t DocumentType) (found int) { } // Sift through the master markdown collection for the specified types. -func (m stashModel) getMarkdownByType(types ...DocumentType) []*markdown { +func (m stashModel) getMarkdownByType(types ...DocType) []*markdown { var agg []*markdown if len(m.markdowns) == 0 { @@ -291,19 +292,19 @@ func (m stashModel) getVisibleMarkdowns() []*markdown { } if m.state == stashStateShowNews { - return m.getMarkdownByType(NewsDocument) + return m.getMarkdownByType(NewsDoc) } - return m.getMarkdownByType(LocalDocument, StashedDocument, ConvertedDocument) + return m.getMarkdownByType(LocalDoc, StashedDoc, ConvertedDoc) } // Return the markdowns eligible to be filtered. func (m stashModel) getFilterableMarkdowns() []*markdown { if m.state == stashStateShowNews { - return m.getMarkdownByType(NewsDocument) + return m.getMarkdownByType(NewsDoc) } - return m.getMarkdownByType(LocalDocument, StashedDocument, ConvertedDocument) + return m.getMarkdownByType(LocalDoc, StashedDoc, ConvertedDoc) } // Command for opening a markdown document in the pager. Note that this also @@ -312,7 +313,7 @@ func (m *stashModel) openMarkdown(md *markdown) tea.Cmd { var cmd tea.Cmd m.state = stashStateLoadingDocument - if md.markdownType == LocalDocument { + if md.markdownType == LocalDoc { cmd = loadLocalMarkdown(md) } else { cmd = loadRemoteMarkdown(m.general.cc, md.ID, md.markdownType) @@ -402,6 +403,7 @@ func newStashModel(general *general) stashModel { filterInput: si, page: 1, paginator: p, + loaded: NewDocTypeSet(), loadingFromNetwork: true, filesStashing: make(map[string]struct{}), } @@ -420,17 +422,17 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { case stashLoadErrMsg: m.err = msg.err - m.loaded |= StashedDocument // still done, albeit unsuccessfully + m.loaded.Add(StashedDoc) // still done, albeit unsuccessfully m.stashFullyLoaded = true m.loadingFromNetwork = false case newsLoadErrMsg: m.err = msg.err - m.loaded |= NewsDocument // still done, albeit unsuccessfully + m.loaded.Add(NewsDoc) // still done, albeit unsuccessfully case localFileSearchFinished: // We're finished searching for local files - m.loaded |= LocalDocument + m.loaded.Add(LocalDoc) case gotStashMsg, gotNewsMsg: // Stash or news results have come in from the server. @@ -441,9 +443,9 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { switch msg := msg.(type) { case gotStashMsg: - m.loaded |= StashedDocument + m.loaded.Add(StashedDoc) m.loadingFromNetwork = false - docs = wrapMarkdowns(StashedDocument, msg) + docs = wrapMarkdowns(StashedDoc, msg) if len(msg) == 0 { // If the server comes back with nothing then we've got @@ -456,8 +458,8 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { } case gotNewsMsg: - m.loaded |= NewsDocument - docs = wrapMarkdowns(NewsDocument, msg) + m.loaded.Add(NewsDoc) + docs = wrapMarkdowns(NewsDoc, msg) } // If we're filtering build filter indexes immediately so any @@ -618,7 +620,7 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { } md := m.selectedMarkdown() - isUserMarkdown := md.markdownType == StashedDocument || md.markdownType == ConvertedDocument + isUserMarkdown := md.markdownType == StashedDoc || md.markdownType == ConvertedDoc isSettingNote := m.selectionState == selectionSettingNote isPromptingDelete := m.selectionState == selectionPromptingDelete @@ -659,7 +661,7 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { md := m.selectedMarkdown() _, isBeingStashed := m.filesStashing[md.localPath] - isLocalMarkdown := md.markdownType == LocalDocument + isLocalMarkdown := md.markdownType == LocalDoc markdownPathMissing := md.localPath == "" if isBeingStashed || !isLocalMarkdown || markdownPathMissing { @@ -692,7 +694,7 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { } t := m.selectedMarkdown().markdownType - if t == StashedDocument || t == ConvertedDocument { + if t == StashedDoc || t == ConvertedDoc { m.selectionState = selectionPromptingDelete } @@ -757,10 +759,10 @@ func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd { continue } - if md.markdownType == ConvertedDocument { + if md.markdownType == ConvertedDoc { // If document was stashed during this session, convert it // back to a local file. - md.markdownType = LocalDocument + md.markdownType = LocalDoc md.Note = stripAbsolutePath(m.markdowns[i].localPath, m.general.cwd) } else { // Delete optimistically and remove the stashed item @@ -995,9 +997,9 @@ func stashHeaderView(m stashModel) string { } } - localItems := m.countMarkdowns(LocalDocument) - stashedItems := m.countMarkdowns(StashedDocument) + m.countMarkdowns(ConvertedDocument) - newsItems := m.countMarkdowns(NewsDocument) + localItems := m.countMarkdowns(LocalDoc) + stashedItems := m.countMarkdowns(StashedDoc) + m.countMarkdowns(ConvertedDoc) + newsItems := m.countMarkdowns(NewsDoc) // Loading's finished and all we have is news. if !loading && localItems == 0 && stashedItems == 0 && newsItems == 0 { @@ -1076,8 +1078,8 @@ func stashHelpView(m stashModel) string { if numDocs > 0 { md := m.selectedMarkdown() - isStashed = md != nil && md.markdownType == StashedDocument - isLocal = md != nil && md.markdownType == LocalDocument + isStashed = md != nil && md.markdownType == StashedDoc + isLocal = md != nil && md.markdownType == LocalDoc } if m.selectionState == selectionSettingNote { @@ -1162,14 +1164,14 @@ func stashHelpViewBuilder(windowWidth int, sections ...string) string { // COMMANDS -func loadRemoteMarkdown(cc *charm.Client, id int, t DocumentType) tea.Cmd { +func loadRemoteMarkdown(cc *charm.Client, id int, t DocType) tea.Cmd { return func() tea.Msg { var ( md *charm.Markdown err error ) - if t == StashedDocument || t == ConvertedDocument { + if t == StashedDoc || t == ConvertedDoc { md, err = cc.GetStashMarkdown(id) } else { md, err = cc.GetNewsMarkdown(id) @@ -1191,7 +1193,7 @@ func loadRemoteMarkdown(cc *charm.Client, id int, t DocumentType) tea.Cmd { func loadLocalMarkdown(md *markdown) tea.Cmd { return func() tea.Msg { - if md.markdownType != LocalDocument { + if md.markdownType != LocalDoc { return errMsg{errors.New("could not load local file: not a local file")} } if md.localPath == "" { @@ -1256,11 +1258,11 @@ func deleteMarkdown(markdowns []*markdown, target *markdown) ([]*markdown, error for i, v := range markdowns { switch target.markdownType { - case LocalDocument, ConvertedDocument: + case LocalDoc, ConvertedDoc: if v.localPath == target.localPath { index = i } - case StashedDocument, NewsDocument: + case StashedDoc, NewsDoc: if v.ID == target.ID { index = i } diff --git a/ui/stashitem.go b/ui/stashitem.go index b9e5304..9a9b273 100644 --- a/ui/stashitem.go +++ b/ui/stashitem.go @@ -27,13 +27,13 @@ func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) { ) switch md.markdownType { - case NewsDocument: + case NewsDoc: if title == "" { title = "News" } else { title = truncate(title, truncateTo) } - case StashedDocument, ConvertedDocument: + case StashedDoc, ConvertedDoc: icon = fileListingStashIcon if title == "" { title = noMemoTitle @@ -80,7 +80,7 @@ func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) { } else { // Regular (non-selected) items - if md.markdownType == NewsDocument { + if md.markdownType == NewsDoc { gutter = " " if isFiltering && m.filterInput.Value() == "" { diff --git a/ui/ui.go b/ui/ui.go index 27a756d..3380bad 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -41,7 +41,7 @@ type Config struct { GlamourStyle string // Which document types shall we show? We work though this with bitmasking. - DocumentTypes DocumentType + DocumentTypes DocTypeSet // For debugging the UI Logfile string `env:"GLOW_LOGFILE"` @@ -189,8 +189,8 @@ func newModel(cfg Config) tea.Model { } } - if cfg.DocumentTypes == 0 { - cfg.DocumentTypes = LocalDocument | StashedDocument | NewsDocument + if len(cfg.DocumentTypes) == 0 { + cfg.DocumentTypes.Add(LocalDoc, StashedDoc, NewsDoc) } general := general{ @@ -209,16 +209,16 @@ func newModel(cfg Config) tea.Model { func (m model) Init() tea.Cmd { var cmds []tea.Cmd - d := &m.general.cfg.DocumentTypes + d := m.general.cfg.DocumentTypes - if *d&StashedDocument != 0 || *d&NewsDocument != 0 { + if d.Contains(StashedDoc) || d.Contains(NewsDoc) { cmds = append(cmds, newCharmClient, spinner.Tick, ) } - if *d&LocalDocument != 0 { + if d.Contains(LocalDoc) { cmds = append(cmds, findLocalFiles(m)) } @@ -336,7 +336,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Even though it failed, news/stash loading is finished - m.stash.loaded |= StashedDocument | NewsDocument + m.stash.loaded.Add(StashedDoc, NewsDoc) m.stash.loadingFromNetwork = false } @@ -351,7 +351,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.keygenState = keygenFinished // Even though it failed, news/stash loading is finished - m.stash.loaded |= StashedDocument | NewsDocument + m.stash.loaded.Add(StashedDoc, NewsDoc) m.stash.loadingFromNetwork = false case keygenSuccessMsg: @@ -636,7 +636,7 @@ func stashDocument(cc *charm.Client, md markdown) tea.Cmd { } // Turn local markdown into a newly stashed (converted) markdown - md.markdownType = ConvertedDocument + md.markdownType = ConvertedDoc md.CreatedAt = time.Now() // Set the note as the filename without the extension @@ -672,7 +672,7 @@ func waitForStatusMessageTimeout(appCtx applicationContext, t *time.Timer) tea.C // already done that. func localFileToMarkdown(cwd string, res gitcha.SearchResult) *markdown { md := &markdown{ - markdownType: LocalDocument, + markdownType: LocalDoc, localPath: res.Path, Markdown: charm.Markdown{ Note: stripAbsolutePath(res.Path, cwd),