From d779ff7e6dcf068fbcf743bed45127c3b857ec92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 3 Jan 2021 00:00:40 +0900 Subject: [PATCH] Make search toggleable - `--phony` renamed to `--disabled` for consistency - `--no-phony` is now `--enabled` - Added `enable-search`, `disable-search`, and `toggle-search` actions for `--bind` - Added `--color` options: `query` and `disabled` Close #2303 --- CHANGELOG.md | 9 ++++++++- README-VIM.md | 30 +++++++++++++++++------------- doc/fzf.txt | 36 ++++++++++++++++++++---------------- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 14 ++++++++++---- src/core.go | 8 +++++--- src/options.go | 16 ++++++++++++---- src/terminal.go | 31 ++++++++++++++++++++++++++----- src/tui/tui.go | 9 +++++++++ test/test_go.rb | 26 ++++++++++++++++++++++++-- 10 files changed, 132 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e650af..69bab6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ CHANGELOG ========= -0.24.5 +0.25.0 ------ - Text attributes set in `--color` are not reset when fzf sees another `--color` option for the same element. This allows you to put custom text @@ -23,6 +23,13 @@ CHANGELOG # Write "regular" if you want to clear the attributes fzf --color hl:176:regular,hl+:177:regular ``` +- Renamed `--phony` to `--disabled` +- You can dynamically enable and disable the search functionality using the + new `enable-search`, `disable-search`, and `toggle-search` actions +- You can assign a different color to the query string for when search is disabled + ```sh + fzf --color query:#ffffff,disabled:#999999 --bind space:toggle-search + ``` - Added `last` action to move the cursor to the last match - The opposite action `top` is renamed to `first`, but `top` is still recognized as a synonym for backward compatibility diff --git a/README-VIM.md b/README-VIM.md index ea588bb..058dfaf 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -171,19 +171,23 @@ list: - `element` is an fzf element to apply a color to: - | Element | Description | - | --- | --- | - | `fg` / `bg` / `hl` | Item (foreground / background / highlight) | - | `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) | - | `hl` / `hl+` | Highlighted substrings (normal / current) | - | `gutter` | Background of the gutter on the left | - | `pointer` | Pointer to the current line (`>`) | - | `marker` | Multi-select marker (`>`) | - | `border` | Border around the window (`--border` and `--preview`) | - | `header` | Header (`--header` or `--header-lines`) | - | `info` | Info line (match counters) | - | `spinner` | Streaming input indicator | - | `prompt` | Prompt before query (`> `) | + | Element | Description | + | --- | --- | + | `fg` / `bg` / `hl` | Item (foreground / background / highlight) | + | `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) | + | `preview-fg` / `preview-bg` | Preview window text and background | + | `hl` / `hl+` | Highlighted substrings (normal / current) | + | `gutter` | Background of the gutter on the left | + | `pointer` | Pointer to the current line (`>`) | + | `marker` | Multi-select marker (`>`) | + | `border` | Border around the window (`--border` and `--preview`) | + | `header` | Header (`--header` or `--header-lines`) | + | `info` | Info line (match counters) | + | `spinner` | Streaming input indicator | + | `query` | Query string | + | `disabled` | Query string when search is disabled | + | `prompt` | Prompt before query (`> `) | + | `pointer` | Pointer to the current line (`>`) | - `component` specifies the component (`fg` / `bg`) from which to extract the color when considering each of the following highlight groups diff --git a/doc/fzf.txt b/doc/fzf.txt index f50a35b..c888896 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -1,4 +1,4 @@ -fzf.txt fzf Last change: December 31 2020 +fzf.txt fzf Last change: January 2 2021 FZF - TABLE OF CONTENTS *fzf* *fzf-toc* ============================================================================== @@ -200,21 +200,25 @@ list: < - `element` is an fzf element to apply a color to: - ----------------------+------------------------------------------------------ - Element | Description ~ - ----------------------+------------------------------------------------------ - `fg` / `bg` / `hl` | Item (foreground / background / highlight) - `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) - `hl` / `hl+` | Highlighted substrings (normal / current) - `gutter` | Background of the gutter on the left - `pointer` | Pointer to the current line ( `>` ) - `marker` | Multi-select marker ( `>` ) - `border` | Border around the window ( `--border` and `--preview` ) - `header` | Header ( `--header` or `--header-lines` ) - `info` | Info line (match counters) - `spinner` | Streaming input indicator - `prompt` | Prompt before query ( `> ` ) - ----------------------+------------------------------------------------------ + ----------------------------+------------------------------------------------------ + Element | Description ~ + ----------------------------+------------------------------------------------------ + `fg` / `bg` / `hl` | Item (foreground / background / highlight) + `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) + `preview-fg` / `preview-bg` | Preview window text and background + `hl` / `hl+` | Highlighted substrings (normal / current) + `gutter` | Background of the gutter on the left + `pointer` | Pointer to the current line ( `>` ) + `marker` | Multi-select marker ( `>` ) + `border` | Border around the window ( `--border` and `--preview` ) + `header` | Header ( `--header` or `--header-lines` ) + `info` | Info line (match counters) + `spinner` | Streaming input indicator + `query` | Query string + `disabled` | Query string when search is disabled + `prompt` | Prompt before query ( `> ` ) + `pointer` | Pointer to the current line ( `>` ) + ----------------------------+------------------------------------------------------ - `component` specifies the component (`fg` / `bg`) from which to extract the color when considering each of the following highlight groups - `group1 [, group2, ...]` is a list of highlight groups that are searched (in diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index f33fafa..904fda3 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf-tmux 1 "Dec 2020" "fzf 0.24.4" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jan 2021" "fzf 0.25.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index e976a5a..5fff2e1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Dec 2020" "fzf 0.24.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2021" "fzf 0.25.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -71,9 +71,10 @@ Transform the presentation of each line using field index expressions .BI "-d, --delimiter=" "STR" Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) .TP -.BI "--phony" +.BI "--disabled" Do not perform search. With this option, fzf becomes a simple selector -interface rather than a "fuzzy finder". +interface rather than a "fuzzy finder". You can later enable the search using +\fBenable-search\fR or `\fBtoggle-search\R action. .SS Search result .TP .B "+s, --no-sort" @@ -323,6 +324,8 @@ color mappings. \fBbg+ \fRBackground (current line) \fBgutter \fRGutter on the left (defaults to \fBbg+\fR) \fBhl+ \fRHighlighted substrings (current line) + \fBquery \fRQuery string + \fBdisabled \fRQuery string when search is disabled \fBinfo \fRInfo line (match counters) \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) \fBprompt \fRPrompt @@ -780,7 +783,9 @@ A key or an event can be bound to one or more of the following actions. \fBdelete-char\fR \fIdel\fR \fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) \fBdeselect-all\fR (deselect all matches) + \fBdisable-search\fR (disable search functionality) \fBdown\fR \fIctrl-j ctrl-n down\fR + \fBenable-search\fR (enable search functionality) \fBend-of-line\fR \fIctrl-e end\fR \fBexecute(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details) @@ -820,6 +825,7 @@ A key or an event can be bound to one or more of the following actions. \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR \fBtoggle-preview-wrap\fR + \fBtoggle-search\fR (toggle search functionality) \fBtoggle-sort\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBunix-line-discard\fR \fIctrl-u\fR @@ -902,7 +908,7 @@ e.g. INITIAL_QUERY="foobar" FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\ fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ - --ansi --phony --query "$INITIAL_QUERY"\fR + --ansi --disabled --query "$INITIAL_QUERY"\fR .SS PREVIEW BINDING diff --git a/src/core.go b/src/core.go index ef470a8..1c009c6 100644 --- a/src/core.go +++ b/src/core.go @@ -237,14 +237,16 @@ func Run(opts *Options, version string, revision string) { go reader.restart(command) } eventBox.Watch(EvtReadNew) + query := []rune{} for { delay := true ticks++ input := func() []rune { - if opts.Phony { - return []rune{} + paused, input := terminal.Input() + if !paused { + query = input } - return []rune(terminal.Input()) + return query } eventBox.Wait(func(events *util.Events) { if _, fin := (*events)[EvtReadFin]; fin { diff --git a/src/options.go b/src/options.go index 691934e..12c0955 100644 --- a/src/options.go +++ b/src/options.go @@ -33,7 +33,7 @@ const usage = `usage: fzf [options] -d, --delimiter=STR Field delimiter regex (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input - --phony Do not perform search + --disabled Do not perform search --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply when the scores are tied [length|begin|end|index] (default: length) @@ -682,8 +682,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { } } switch components[0] { - case "input": + case "query", "input": mergeAttr(&theme.Input) + case "disabled": + mergeAttr(&theme.Disabled) case "fg": mergeAttr(&theme.Fg) case "bg": @@ -875,6 +877,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) { appendAction(actToggleOut) case "toggle-all": appendAction(actToggleAll) + case "toggle-search": + appendAction(actToggleSearch) case "select-all": appendAction(actSelectAll) case "deselect-all": @@ -923,6 +927,10 @@ func parseKeymap(keymap map[tui.Event][]action, str string) { appendAction(actPreviewHalfPageUp) case "preview-half-page-down": appendAction(actPreviewHalfPageDown) + case "enable-search": + appendAction(actEnableSearch) + case "disable-search": + appendAction(actDisableSearch) default: t := isExecuteAction(specLower) if t == actIgnore { @@ -1199,9 +1207,9 @@ func parseOptions(opts *Options, allArgs []string) { } case "--no-expect": opts.Expect = make(map[tui.Event]string) - case "--no-phony": + case "--enabled", "--no-phony": opts.Phony = false - case "--phony": + case "--disabled", "--phony": opts.Phony = true case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) diff --git a/src/terminal.go b/src/terminal.go index e630db4..4801162 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -125,6 +125,7 @@ type Terminal struct { unicode bool borderShape tui.BorderShape cleanExit bool + paused bool border tui.Window window tui.Window pborder tui.Window @@ -233,6 +234,7 @@ const ( actSelectAll actDeselectAll actToggle + actToggleSearch actToggleAll actToggleDown actToggleUp @@ -270,6 +272,8 @@ const ( actFirst actLast actReload + actDisableSearch + actEnableSearch ) type placeholderFlags struct { @@ -488,6 +492,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { unicode: opts.Unicode, borderShape: opts.BorderShape, cleanExit: opts.ClearOnExit, + paused: opts.Phony, strong: strongAttr, cycle: opts.Cycle, header: header, @@ -563,10 +568,10 @@ func (t *Terminal) noInfoLine() bool { } // Input returns current query string -func (t *Terminal) Input() []rune { +func (t *Terminal) Input() (bool, []rune) { t.mutex.Lock() defer t.mutex.Unlock() - return copySlice(t.input) + return t.paused, copySlice(t.input) } // UpdateCount updates the count information @@ -925,8 +930,12 @@ func (t *Terminal) printPrompt() { t.prompt() before, after := t.updatePromptOffset() - t.window.CPrint(tui.ColInput, string(before)) - t.window.CPrint(tui.ColInput, string(after)) + color := tui.ColInput + if t.paused { + color = tui.ColDisabled + } + t.window.CPrint(color, string(before)) + t.window.CPrint(color, string(after)) } func (t *Terminal) trimMessage(message string, maxWidth int) string { @@ -1880,7 +1889,8 @@ func (t *Terminal) Loop() { version++ // We don't display preview window if no match if items[0] != nil { - command := t.replacePlaceholder(commandTemplate, false, string(t.Input()), items) + _, query := t.Input() + command := t.replacePlaceholder(commandTemplate, false, string(query), items) initialOffset := 0 cmd := util.ExecCommand(command, true) if pwindow != nil { @@ -2465,6 +2475,17 @@ func (t *Terminal) Loop() { t.input = trimQuery(t.history.next()) t.cx = len(t.input) } + case actToggleSearch: + t.paused = !t.paused + changed = !t.paused + req(reqPrompt) + case actEnableSearch: + t.paused = false + changed = true + req(reqPrompt) + case actDisableSearch: + t.paused = true + req(reqPrompt) case actSigStop: p, err := os.FindProcess(os.Getpid()) if err == nil { diff --git a/src/tui/tui.go b/src/tui/tui.go index cc9c7f6..eb09da4 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -250,6 +250,7 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair { type ColorTheme struct { Colored bool Input ColorAttr + Disabled ColorAttr Fg ColorAttr Bg ColorAttr PreviewFg ColorAttr @@ -421,6 +422,7 @@ var ( ColPrompt ColorPair ColNormal ColorPair ColInput ColorPair + ColDisabled ColorPair ColMatch ColorPair ColCursor ColorPair ColCursorEmpty ColorPair @@ -443,6 +445,7 @@ func EmptyTheme() *ColorTheme { return &ColorTheme{ Colored: true, Input: ColorAttr{colUndefined, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined}, @@ -465,6 +468,7 @@ func NoColorTheme() *ColorTheme { return &ColorTheme{ Colored: false, Input: ColorAttr{colDefault, AttrRegular}, + Disabled: ColorAttr{colDefault, AttrRegular}, Fg: ColorAttr{colDefault, AttrRegular}, Bg: ColorAttr{colDefault, AttrRegular}, PreviewFg: ColorAttr{colDefault, AttrRegular}, @@ -492,6 +496,7 @@ func init() { Default16 = &ColorTheme{ Colored: true, Input: ColorAttr{colDefault, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined}, @@ -511,6 +516,7 @@ func init() { Dark256 = &ColorTheme{ Colored: true, Input: ColorAttr{colDefault, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined}, @@ -530,6 +536,7 @@ func init() { Light256 = &ColorTheme{ Colored: true, Input: ColorAttr{colDefault, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined}, PreviewFg: ColorAttr{colUndefined, AttrUndefined}, @@ -564,6 +571,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { return c } theme.Input = o(baseTheme.Input, theme.Input) + theme.Disabled = o(theme.Input, o(baseTheme.Disabled, theme.Disabled)) theme.Fg = o(baseTheme.Fg, theme.Fg) theme.Bg = o(baseTheme.Bg, theme.Bg) theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg)) @@ -597,6 +605,7 @@ func initPalette(theme *ColorTheme) { ColPrompt = pair(theme.Prompt, theme.Bg) ColNormal = pair(theme.Fg, theme.Bg) ColInput = pair(theme.Input, theme.Bg) + ColDisabled = pair(theme.Disabled, theme.Bg) ColMatch = pair(theme.Match, theme.Bg) ColCursor = pair(theme.Cursor, theme.Gutter) ColCursorEmpty = pair(blank, theme.Gutter) diff --git a/test/test_go.rb b/test/test_go.rb index f145675..aa28e1c 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1658,13 +1658,35 @@ class TestGoFZF < TestBase tmux.until { |lines| assert_includes lines[1], ' + green ' } end - def test_phony - tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter + def test_disabled + tmux.send_keys %(seq 1000 | #{FZF} --query 333 --disabled --bind a:enable-search,b:disable-search,c:toggle-search --preview 'echo {} {q}'), :Enter tmux.until { |lines| assert_equal 1000, lines.match_count } tmux.until { |lines| assert_includes lines[1], ' 1 333 ' } tmux.send_keys 'foo' tmux.until { |lines| assert_equal 1000, lines.match_count } tmux.until { |lines| assert_includes lines[1], ' 1 333foo ' } + + # Already disabled, no change + tmux.send_keys 'b' + tmux.until { |lines| assert_equal 1000, lines.match_count } + + # Enable search + tmux.send_keys 'a' + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.send_keys :BSpace, :BSpace, :BSpace + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' 333 333 ' } + + # Toggle search -> disabled again, but retains the previous result + tmux.send_keys 'c' + tmux.send_keys 'foo' + tmux.until { |lines| assert_includes lines[1], ' 333 333foo ' } + tmux.until { |lines| assert_equal 1, lines.match_count } + + # Enabled, no match + tmux.send_keys 'c' + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' 333foo ' } end def test_reload