diff options
Diffstat (limited to 'src/terminal.go')
| -rw-r--r-- | src/terminal.go | 225 |
1 files changed, 192 insertions, 33 deletions
diff --git a/src/terminal.go b/src/terminal.go index 1f0a6e41..96b06259 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -274,9 +274,11 @@ type Terminal struct { footerLabelLen int footerLabelOpts labelOpts gutterReverse bool + gutterRawReverse bool pointer string pointerLen int pointerEmpty string + pointerEmptyRaw string marker string markerLen int markerEmpty string @@ -297,6 +299,7 @@ type Terminal struct { subWordNext string cx int cy int + lastMatchingIndex int32 offset int xoffset int yanked []rune @@ -383,6 +386,9 @@ type Terminal struct { printer func(string) printsep string merger *Merger + passMerger *Merger + resultMerger *Merger + matchMap map[int32]Result selected map[int32]selectedItem version int64 revision revision @@ -429,6 +435,7 @@ type Terminal struct { clickFooterColumn int proxyScript string numLinesCache map[int32]numLinesCacheValue + raw bool } type numLinesCacheValue struct { @@ -569,13 +576,16 @@ const ( actToggleWrap actToggleMultiLine actToggleHscroll + actToggleRaw actTrackCurrent actToggleInput actHideInput actShowInput actUntrackCurrent actDown + actDownMatch actUp + actUpMatch actPageUp actPageDown actPosition @@ -796,8 +806,10 @@ func defaultKeymap() map[tui.Event][]*action { add(tui.CtrlK, actUp) add(tui.CtrlL, actClearScreen) add(tui.Enter, actAccept) - add(tui.CtrlN, actDown) - add(tui.CtrlP, actUp) + add(tui.CtrlN, actDownMatch) + add(tui.CtrlP, actUpMatch) + add(tui.AltDown, actDownMatch) + add(tui.AltUp, actUpMatch) add(tui.CtrlU, actUnixLineDiscard) add(tui.CtrlW, actUnixWordRubout) add(tui.CtrlY, actYank) @@ -953,6 +965,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor } keymapCopy := maps.Clone(opts.Keymap) + em := EmptyMerger(revision{}) t := Terminal{ initDelay: delay, infoCommand: opts.InfoCommand, @@ -980,6 +993,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor subWordNext: subWordNext, cx: len(input), cy: 0, + lastMatchingIndex: minItem.Index(), offset: 0, xoffset: 0, yanked: []rune{}, @@ -1039,6 +1053,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor nth: opts.Nth, nthCurrent: opts.Nth, tabstop: opts.Tabstop, + raw: opts.Raw, hasStartActions: false, hasResultActions: false, hasFocusActions: false, @@ -1052,7 +1067,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor printer: opts.Printer, printsep: opts.PrintSep, proxyScript: opts.ProxyScript, - merger: EmptyMerger(revision{}), + merger: em, + passMerger: em, + resultMerger: em, + matchMap: make(map[int32]Result), selected: make(map[int32]selectedItem), runningCmds: util.NewConcurrentSet[*runningCmd](), reqBox: util.NewEventBox(), @@ -1093,7 +1111,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black, opts.InputBorderShape.Visible(), opts.HeaderBorderShape.Visible()) // Gutter character - var gutterChar string + var gutterChar, gutterRawChar string if opts.Gutter != nil { gutterChar = *opts.Gutter } else if t.unicode && !t.theme.Gutter.Color.IsDefault() { @@ -1103,12 +1121,24 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor t.gutterReverse = true } + if opts.GutterRaw != nil { + gutterRawChar = *opts.GutterRaw + } else if t.unicode && !t.theme.Gutter.Color.IsDefault() { + // TODO: Doesn't look too good. Maybe use a different color instead, or both? + gutterRawChar = "▖" + } else { + gutterRawChar = ":" + t.gutterRawReverse = false + } + t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) // Pre-calculated empty pointer and marker signs if t.pointerLen == 0 { t.pointerEmpty = "" + t.pointerEmptyRaw = "" } else { t.pointerEmpty = gutterChar + strings.Repeat(" ", util.Max(0, t.pointerLen-1)) + t.pointerEmptyRaw = gutterRawChar + strings.Repeat(" ", util.Max(0, t.pointerLen-1)) } t.markerEmpty = strings.Repeat(" ", t.markerLen) @@ -1282,7 +1312,7 @@ func (t *Terminal) environImpl(forPreview bool) []string { } env = append(env, "FZF_INPUT_STATE="+inputState) env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count)) - env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length())) + env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length())) env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected))) env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines)) env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns)) @@ -1693,7 +1723,8 @@ func (t *Terminal) UpdateProgress(progress float32) { } // UpdateList updates Merger to display the list -func (t *Terminal) UpdateList(merger *Merger) { +func (t *Terminal) UpdateList(result MatchResult) { + merger := result.merger t.mutex.Lock() prevIndex := minItem.Index() newRevision := merger.Revision() @@ -1706,6 +1737,15 @@ func (t *Terminal) UpdateList(merger *Merger) { } t.progress = 100 t.merger = merger + t.resultMerger = merger + t.passMerger = result.passMerger + if t.raw { + t.merger = result.passMerger + t.matchMap = t.resultMerger.ToMap() + } else { + t.merger = result.merger + t.matchMap = make(map[int32]Result) + } if t.revision != newRevision { if !t.revision.compatible(newRevision) { // Reloaded: clear selection @@ -1754,7 +1794,7 @@ func (t *Terminal) UpdateList(merger *Merger) { } needActivation := false if !t.reading { - switch t.merger.Length() { + switch t.resultMerger.Length() { case 0: zero := tui.Zero.AsEvent() if _, prs := t.keymap[zero]; prs { @@ -2801,7 +2841,7 @@ func (t *Terminal) printInfoImpl() { return } - found := t.merger.Length() + found := t.resultMerger.Length() total := util.Max(found, t.count) output := fmt.Sprintf("%d/%d", found, total) if t.toggleSort { @@ -3122,12 +3162,16 @@ func (t *Terminal) gutter(current bool) { var color tui.ColorPair if current { color = tui.ColCurrentCursorEmpty - } else if t.gutterReverse { + } else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse { color = tui.ColCursorEmpty } else { color = tui.ColCursorEmptyChar } - t.window.CPrint(color, t.pointerEmpty) + gutter := t.pointerEmpty + if t.raw { + gutter = t.pointerEmptyRaw + } + t.window.CPrint(color, gutter) } func (t *Terminal) renderGapLine(line int, barRange [2]int, drawLine bool) { @@ -3162,7 +3206,11 @@ func (t *Terminal) printList() { for line, itemCount := startLine, 0; line <= maxy; line, itemCount = line+1, itemCount+1 { if itemCount < count { item := t.merger.Get(itemCount + t.offset) - line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange) + current := itemCount == t.cy-t.offset + if current && (!t.raw || t.isItemMatch(item.item)) { + t.lastMatchingIndex = item.Index() + } + line = t.printItem(item, line, maxy, itemCount, current, barRange) } else if !t.prevLines[line].empty { t.renderEmptyLine(line, barRange) } @@ -3184,6 +3232,14 @@ func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool func (t *Terminal) printItem(result Result, line int, maxLine int, index int, current bool, barRange [2]int) int { item := result.item + matched := true + var matchResult Result + if t.raw { + if matchResult, matched = t.matchMap[item.Index()]; matched { + result = matchResult + } + } + _, selected := t.selected[item.Index()] label := "" extraWidth := 0 @@ -3310,7 +3366,13 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu } return indentSize } - finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask) + base := tui.ColCurrent + match := tui.ColCurrentMatch + if !matched { + base = base.WithFg(t.theme.Hidden) + match = match.WithFg(t.theme.Hidden) + } + finalLineNum = t.printHighlighted(result, base, match, true, true, line, maxLine, forceRedraw, preTask, postTask) } else { preTask := func(marker markerClass) int { w := t.window.Width() - t.pointerLen @@ -3344,6 +3406,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu base = base.WithBg(altBg) match = match.WithBg(altBg) } + if !matched { + base = base.WithFg(t.theme.Hidden) + match = match.WithFg(t.theme.Hidden) + } finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask) } for i := 0; i < t.gap && finalLineNum < maxLine; i++ { @@ -3396,8 +3462,8 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat item := result.item matchOffsets := []Offset{} var pos *[]int - if match && t.merger.pattern != nil { - _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) + if match && t.resultMerger.pattern != nil { + _, matchOffsets, pos = t.resultMerger.pattern.MatchItem(item, true, t.slab) } charOffsets := matchOffsets if pos != nil { @@ -3429,7 +3495,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat } if !wholeCovered && t.nthAttr > 0 { var tokens []Token - if item.transformed != nil && item.transformed.revision == t.merger.revision { + if item.transformed != nil && item.transformed.revision == t.resultMerger.revision { tokens = item.transformed.tokens } else { tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent) @@ -4677,6 +4743,33 @@ func (t *Terminal) currentItem() *Item { return nil } +func (t *Terminal) isCurrentItemMatch() bool { + cnt := t.merger.Length() + if t.cy >= 0 && cnt > 0 && cnt > t.cy { + if !t.raw { + return true + } + item := t.merger.Get(t.cy).item + return t.isItemMatch(item) + } + return false +} + +func (t *Terminal) isItemMatch(item *Item) bool { + _, matched := t.matchMap[item.Index()] + return matched +} + +func (t *Terminal) filterSelected() { + filtered := make(map[int32]selectedItem) + for k, v := range t.selected { + if t.isItemMatch(v.item) { + filtered[k] = v + } + } + t.selected = filtered +} + func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*Item) { current := t.currentItem() slot, plus, asterisk, forceUpdate := hasPreviewFlags(template) @@ -4721,6 +4814,10 @@ func (t *Terminal) selectItem(item *Item) bool { if len(t.selected) >= t.multi { return false } + // TODO: Should we allow selecting non-matching items? + // if t.raw && !t.isItemMatch(item) { + // return false + // } if _, found := t.selected[item.Index()]; found { return true } @@ -5917,8 +6014,9 @@ func (t *Terminal) Loop() error { } case actSelectAll: if t.multi > 0 { - for i := 0; i < t.merger.Length(); i++ { - if !t.selectItem(t.merger.Get(i).item) { + // Limit the scope only to the matching items + for i := 0; i < t.resultMerger.Length(); i++ { + if !t.selectItem(t.resultMerger.Get(i).item) { break } } @@ -5926,8 +6024,10 @@ func (t *Terminal) Loop() error { } case actDeselectAll: if t.multi > 0 { - for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { - t.deselectItem(t.merger.Get(i).item) + // Also limit the scope only to the matching items, while this may + // not be straightforward in raw mode. + for i := 0; i < t.resultMerger.Length() && len(t.selected) > 0; i++ { + t.deselectItem(t.resultMerger.Get(i).item) } req(reqList, reqInfo) } @@ -5955,17 +6055,17 @@ func (t *Terminal) Loop() error { case actToggleAll: if t.multi > 0 { prevIndexes := make(map[int]struct{}) - for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { - item := t.merger.Get(i).item + for i := 0; i < t.resultMerger.Length() && len(t.selected) > 0; i++ { + item := t.resultMerger.Get(i).item if _, found := t.selected[item.Index()]; found { prevIndexes[i] = struct{}{} t.deselectItem(item) } } - for i := 0; i < t.merger.Length(); i++ { + for i := 0; i < t.resultMerger.Length(); i++ { if _, found := prevIndexes[i]; !found { - item := t.merger.Get(i).item + item := t.resultMerger.Get(i).item if !t.selectItem(item) { break } @@ -5993,19 +6093,77 @@ func (t *Terminal) Loop() error { t.vmove(1, true) req(reqList) } - case actDown: - t.vmove(-1, true) + case actDown, actDownMatch: + if t.raw && a.t == actDownMatch { + if t.resultMerger.Length() > 0 { + prevCy := t.cy + for t.vmove(-1, true) && !t.isCurrentItemMatch() { + } + if !t.isCurrentItemMatch() { + t.vset(prevCy) + } + } + } else { + t.vmove(-1, true) + } req(reqList) - case actUp: - t.vmove(1, true) + case actUp, actUpMatch: + if t.raw && a.t == actUpMatch { + if t.resultMerger.Length() > 0 { + prevCy := t.cy + for t.vmove(1, true) && !t.isCurrentItemMatch() { + } + if !t.isCurrentItemMatch() { + t.vset(prevCy) + } + } + } else { + t.vmove(1, true) + } + req(reqList) + case actToggleRaw: + t.raw = !t.raw + prevPos := t.cy - t.offset + prevIndex := t.currentIndex() + if t.raw { + // Build matchMap if not available + if len(t.matchMap) == 0 { + t.matchMap = t.resultMerger.ToMap() + } + t.merger = t.passMerger + } else { + t.merger = t.resultMerger + + // Need to remove non-matching items from the selection + if t.multi > 0 && len(t.selected) > 0 { + t.filterSelected() + req(reqInfo) + } + } + + // Try to retain position + if prevIndex != minItem.Index() { + i := t.merger.FindIndex(prevIndex) + if i >= 0 { + t.cy = i + } else { + t.cy = t.merger.FindIndex(t.lastMatchingIndex) + } + t.offset = t.cy - prevPos + } + + // List needs to be rerendered + t.forceRerenderList() req(reqList) case actAccept: req(reqClose) case actAcceptNonEmpty: + // TODO: Allow accepting unmatched items in raw mode? if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 { req(reqClose) } case actAcceptOrPrintQuery: + // TODO: Allow accepting unmatched items in raw mode? if len(t.selected) > 0 || t.merger.Length() > 0 { req(reqClose) } else { @@ -6841,7 +6999,7 @@ func (t *Terminal) Loop() error { reload := changed || newCommand != nil var reloadRequest *searchRequest if reload { - reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()} + reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.resultMerger.Revision()} } // Dispatch queued background requests @@ -6961,7 +7119,8 @@ func (t *Terminal) constrain() { } } -func (t *Terminal) vmove(o int, allowCycle bool) { +// Returns true if the cursor position is successfully updated +func (t *Terminal) vmove(o int, allowCycle bool) bool { if t.layout != layoutDefault { o *= -1 } @@ -6978,7 +7137,7 @@ func (t *Terminal) vmove(o int, allowCycle bool) { } } } - t.vset(dest) + return t.vset(dest) } func (t *Terminal) vset(o int) bool { @@ -7045,9 +7204,9 @@ func (t *Terminal) dumpStatus(params getParams) string { selected[i] = t.dumpItem(selectedItems[i+params.offset].item) } - matches := make([]StatusItem, util.Max(0, util.Min(params.limit, t.merger.Length()-params.offset))) + matches := make([]StatusItem, util.Max(0, util.Min(params.limit, t.resultMerger.Length()-params.offset))) for i := range matches { - matches[i] = t.dumpItem(t.merger.Get(i + params.offset).item) + matches[i] = t.dumpItem(t.resultMerger.Get(i + params.offset).item) } var current *StatusItem @@ -7064,7 +7223,7 @@ func (t *Terminal) dumpStatus(params getParams) string { Position: t.cy, Sort: t.sort, TotalCount: t.count, - MatchCount: t.merger.Length(), + MatchCount: t.resultMerger.Length(), Current: current, Matches: matches, Selected: selected, |
