Replace bit-based document type implementation with a set-based one

This commit is contained in:
Christian Rocha 2020-11-27 21:52:51 -05:00
parent 7fabcea06d
commit 4c63c62bfa
8 changed files with 189 additions and 69 deletions

View file

@ -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

78
ui/doctypes.go Normal file
View file

@ -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
}

48
ui/doctypes_test.go Normal file
View file

@ -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)
}
}

View file

@ -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,

View file

@ -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)
}

View file

@ -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
}

View file

@ -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() == "" {

View file

@ -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),