Tag autocomplete shows suggestions when typing #partial in capture bar. Tab/enter accepts, up/down navigates, esc dismisses. Query composition extends ? search with date filters (@today, @week, @month, <7d, >30d), card type filters (^snippet), all composable with existing text and tag filters.
This commit is contained in:
+135
-18
@@ -79,26 +79,30 @@ type model struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
list listModel
|
||||
cards cardsModel
|
||||
detail detailModel
|
||||
input inputModel
|
||||
filter filterModel
|
||||
promote promoteModel
|
||||
absorb absorbModel
|
||||
tagRail tagRailModel
|
||||
stumble stumbleModel
|
||||
showHelp bool
|
||||
list listModel
|
||||
cards cardsModel
|
||||
detail detailModel
|
||||
input inputModel
|
||||
filter filterModel
|
||||
promote promoteModel
|
||||
absorb absorbModel
|
||||
tagRail tagRailModel
|
||||
stumble stumbleModel
|
||||
showHelp bool
|
||||
autocomplete autocompleteModel
|
||||
|
||||
focus focusPane
|
||||
splitDetail bool
|
||||
showTagRail bool
|
||||
|
||||
filterTag string
|
||||
confirmID string
|
||||
cardsSort cardsSort
|
||||
searchQuery string
|
||||
searchTags []string
|
||||
filterTag string
|
||||
confirmID string
|
||||
cardsSort cardsSort
|
||||
searchQuery string
|
||||
searchTags []string
|
||||
queryDateFrom *string
|
||||
queryDateTo *string
|
||||
queryCardType *db.CardType
|
||||
|
||||
status string
|
||||
statusSeq int
|
||||
@@ -149,6 +153,15 @@ func (m model) listParams() db.ListParams {
|
||||
if m.filterTag != "" {
|
||||
p.Tag = &m.filterTag
|
||||
}
|
||||
if m.queryDateFrom != nil {
|
||||
p.From = m.queryDateFrom
|
||||
}
|
||||
if m.queryDateTo != nil {
|
||||
p.To = m.queryDateTo
|
||||
}
|
||||
if m.queryCardType != nil {
|
||||
p.CardTypeFilter = m.queryCardType
|
||||
}
|
||||
if m.mode == modeCards {
|
||||
p.CardsOnly = true
|
||||
switch m.cardsSort {
|
||||
@@ -167,7 +180,7 @@ func (m model) listParams() db.ListParams {
|
||||
}
|
||||
|
||||
func (m model) hasSearch() bool {
|
||||
return m.searchQuery != "" || len(m.searchTags) > 0
|
||||
return m.searchQuery != "" || len(m.searchTags) > 0 || m.queryDateFrom != nil || m.queryDateTo != nil || m.queryCardType != nil
|
||||
}
|
||||
|
||||
func (m *model) applySearch() {
|
||||
@@ -363,6 +376,10 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m model) updateCapture(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if m.autocomplete.active {
|
||||
m.acceptAutocomplete()
|
||||
return m, nil
|
||||
}
|
||||
val := m.input.ti.Value()
|
||||
if val == "" {
|
||||
cmd := m.setFocus(focusList)
|
||||
@@ -375,23 +392,92 @@ func (m model) updateCapture(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if result.query {
|
||||
m.searchQuery = result.body
|
||||
m.searchTags = result.tags
|
||||
m.queryDateFrom = result.dateFrom
|
||||
m.queryDateTo = result.dateTo
|
||||
m.queryCardType = result.cardType
|
||||
m.input.clearText()
|
||||
m.autocomplete.active = false
|
||||
if result.dateFrom != nil || result.dateTo != nil || result.cardType != nil {
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, tea.Batch(cmd, loadEntities(m.store, m.listParams()))
|
||||
}
|
||||
m.applySearch()
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
}
|
||||
if result.entity != nil {
|
||||
m.autocomplete.active = false
|
||||
return m, createEntity(m.store, result.entity)
|
||||
}
|
||||
return m, nil
|
||||
case "esc", "tab":
|
||||
case "tab":
|
||||
if m.autocomplete.active {
|
||||
m.acceptAutocomplete()
|
||||
return m, nil
|
||||
}
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
case "esc":
|
||||
if m.autocomplete.active {
|
||||
m.autocomplete.active = false
|
||||
return m, nil
|
||||
}
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
case "up":
|
||||
if m.autocomplete.active {
|
||||
m.autocomplete.moveUp()
|
||||
return m, nil
|
||||
}
|
||||
case "down":
|
||||
if m.autocomplete.active {
|
||||
m.autocomplete.moveDown()
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
m.input = m.input.updateKey(msg)
|
||||
m.updateAutocompleteSuggestions()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *model) updateAutocompleteSuggestions() {
|
||||
val := m.input.ti.Value()
|
||||
pos := m.input.ti.Position()
|
||||
start, end, prefix, ok := tagTokenAtCursor(val, pos)
|
||||
if !ok || prefix == "" {
|
||||
m.autocomplete.active = false
|
||||
return
|
||||
}
|
||||
suggestions := filterTagSuggestions(m.tagRail.tags, prefix)
|
||||
if len(suggestions) == 0 {
|
||||
m.autocomplete.active = false
|
||||
return
|
||||
}
|
||||
m.autocomplete.suggestions = suggestions
|
||||
m.autocomplete.prefix = prefix
|
||||
m.autocomplete.tokenStart = start
|
||||
m.autocomplete.tokenEnd = end
|
||||
m.autocomplete.active = true
|
||||
if m.autocomplete.cursor >= len(suggestions) {
|
||||
m.autocomplete.cursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) acceptAutocomplete() {
|
||||
if !m.autocomplete.active || len(m.autocomplete.suggestions) == 0 {
|
||||
return
|
||||
}
|
||||
selected := m.autocomplete.selected()
|
||||
if selected == "" {
|
||||
return
|
||||
}
|
||||
val := m.input.ti.Value()
|
||||
newVal := val[:m.autocomplete.tokenStart] + "#" + selected + " " + val[m.autocomplete.tokenEnd:]
|
||||
m.input.ti.SetValue(newVal)
|
||||
m.input.ti.SetCursor(m.autocomplete.tokenStart + 1 + len(selected) + 1)
|
||||
m.autocomplete.active = false
|
||||
}
|
||||
|
||||
func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// Tag rail focus handling
|
||||
if m.focus == focusTagRail && m.state == stateList {
|
||||
@@ -636,9 +722,16 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
if m.state == stateList && m.hasSearch() {
|
||||
hadDBFilters := m.queryDateFrom != nil || m.queryDateTo != nil || m.queryCardType != nil
|
||||
m.searchQuery = ""
|
||||
m.searchTags = nil
|
||||
m.queryDateFrom = nil
|
||||
m.queryDateTo = nil
|
||||
m.queryCardType = nil
|
||||
m.status = ""
|
||||
if hadDBFilters {
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
}
|
||||
if m.mode == modeCards {
|
||||
m.cards.applyFilter()
|
||||
} else {
|
||||
@@ -953,6 +1046,10 @@ func (m model) View() string {
|
||||
|
||||
content = lipgloss.NewStyle().Width(m.width).Height(m.contentHeight()).Render(content)
|
||||
|
||||
acView := m.autocomplete.view(m.width)
|
||||
if acView != "" {
|
||||
return header + "\n" + content + "\n" + acView + "\n" + captureBar + "\n" + statusLine
|
||||
}
|
||||
return header + "\n" + content + "\n" + captureBar + "\n" + statusLine
|
||||
}
|
||||
|
||||
@@ -994,6 +1091,15 @@ func (m model) headerView() string {
|
||||
for _, t := range m.searchTags {
|
||||
pill += " #" + t
|
||||
}
|
||||
if m.queryDateFrom != nil {
|
||||
pill += " from:" + *m.queryDateFrom
|
||||
}
|
||||
if m.queryDateTo != nil {
|
||||
pill += " to:" + *m.queryDateTo
|
||||
}
|
||||
if m.queryCardType != nil {
|
||||
pill += " ^" + string(*m.queryCardType)
|
||||
}
|
||||
header += " " + searchPillStyle.Render(pill)
|
||||
}
|
||||
|
||||
@@ -1021,7 +1127,18 @@ func (m model) statusLine() string {
|
||||
}
|
||||
|
||||
func (m model) contentHeight() int {
|
||||
return m.height - 4
|
||||
h := m.height - 4
|
||||
if m.autocomplete.active && len(m.autocomplete.suggestions) > 0 {
|
||||
n := m.autocomplete.visibleCount()
|
||||
if len(m.autocomplete.suggestions) > maxSuggestions {
|
||||
n++
|
||||
}
|
||||
h -= n + 1
|
||||
}
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (m *model) recalcSizes() {
|
||||
|
||||
Reference in New Issue
Block a user