diff --git a/README.md b/README.md index 38b64ee..637d30c 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ it; in the document pane it is that document. In the browse pane: | `f` | fullscreen the focused pane | | `Alt-h` / `Alt-l` | focus the list / the document, from the editor too | | `j` / `k` | move down / up, or scroll the document when it has focus | -| `gg` / `G` | first / last entry, or top / bottom of the document | +| `gg` / `G` | first / last entry, or top / bottom of the document; a first `g`, `d`, `]` or `[` shows in the status line for a second while it waits for the second key | | `Ctrl-D` / `Ctrl-U` | half a page down / up the open document | | `]]` / `[[` | the next / previous heading of the open document, in the editor too | | `gO` | the document's headings as a list — `j`/`k`, `Enter` jumps there (`:outline`) | @@ -94,8 +94,9 @@ it; in the document pane it is that document. In the browse pane: | `c` | capture a card into this space's inbox without leaving where you are | | `Esc`, `q` | close the document | | `h` | up one level in the tree | -| `/` | full-text search in this space (`n` clears it); `Tab` or `Ctrl-N`/`Ctrl-P` walk the results without leaving the line, `Enter` opens the one lit | +| `/` | full-text search in this space (`:noh` clears it); `Tab` or `Ctrl-N`/`Ctrl-P` walk the results without leaving the line, `Enter` opens the one lit | | `Ctrl-P` | full-text search across every space; `Enter` goes to the match | +| `n` / `N` | the next / previous search result, opened beside the results; past the last it starts again at the first | | `s` / `y` | toggle sharing / mint a new share link — either asks first when it would cut readers off | | `x`, `dd` | delete the entry under the cursor (asks `y`/`N`) | | `:` | command line — `Tab` / `Shift-Tab` cycle the matching commands, then the argument (folders for `:cd` and `:mv`, spaces for `:space`, options for `:set` …); `↑` / `↓` recall the lines run before that start as this one does | diff --git a/web/src/App.svelte b/web/src/App.svelte index 4b02fed..e08a1b1 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -44,6 +44,7 @@ let importRoot = false let shell = $state() let overlayEl = $state() + /** The first half of `gg`, `dd`, `]]` …, shown until the second comes. */ let pending = $state('') /** `F`: the pane whose links and buttons are wearing hints right now. */ let hintRoot = $state(null) @@ -135,6 +136,13 @@ return () => clearTimeout(timer) }) + // A first half left alone is forgotten, as vim's `timeoutlen` does. + $effect(() => { + if (!pending) return + const timer = setTimeout(() => (pending = ''), 1000) + return () => clearTimeout(timer) + }) + // Guard against losing a half-written entry to a stray refresh. $effect(() => { if (!workspace.dirty) return @@ -775,8 +783,11 @@ })() break case 'n': + case 'N': + // Through the results, as vim's `n` goes through the matches; the + // search itself stays until `:noh`. event.preventDefault() - if (workspace.query) void workspace.guard(() => workspace.search('')) + void workspace.guard(() => workspace.nextResult(key === 'n' ? 1 : -1)) break case '/': event.preventDefault() @@ -864,7 +875,7 @@ {/key} - + {#if cmdline !== null} ((cmdline = null), (cmdGlobal = false))} /> diff --git a/web/src/lib/Help.svelte b/web/src/lib/Help.svelte index c6caa57..6d709e1 100644 --- a/web/src/lib/Help.svelte +++ b/web/src/lib/Help.svelte @@ -10,7 +10,7 @@ ['f', 'fullscreen the focused pane; again or Esc to leave'], ['Alt-h / Alt-l', 'focus the list / the document, from the editor too'], ['j / k · ↓ ↑', 'move down / up the list — scroll the document when it has focus'], - ['gg / G', 'first / last row, or top / bottom of the document'], + ['gg / G', 'first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line'], ['Ctrl-D / Ctrl-U', 'half a page down / up the open document'], [']] / [[', 'next / previous heading of the document — in the editor too'], ['gO', 'the headings as a list: j/k, Enter jumps there'], @@ -33,7 +33,7 @@ ['c', "capture a card into this space's inbox, without leaving here"], ['Esc · q', 'close the document'], ['/', 'full-text search, inside the current space — Tab or Ctrl-N/P walk the results, Enter opens one'], - ['n', 'clear the search'], + ['n / N', 'next / previous search result, opened beside the results — :noh clears the search'], ['s', 'toggle sharing for the document — asks before taking it back'], ['y', 'mint a new share link and copy it — asks, the old one stops working'], ['x · dd', 'delete what has focus — the row in the list, the document in its pane (asks y/N)'], diff --git a/web/src/lib/StatusBar.svelte b/web/src/lib/StatusBar.svelte index c7b1b03..4b10c47 100644 --- a/web/src/lib/StatusBar.svelte +++ b/web/src/lib/StatusBar.svelte @@ -3,6 +3,9 @@ import { workspace } from './store.svelte' import { isTouchDevice } from './util' + /** A key waiting for its second half, like vim's `showcmd`. */ + const { pending = '' }: { pending?: string } = $props() + const mode = $derived( workspace.editing ? (workspace.vimEnabled ? workspace.vimMode.toUpperCase() : 'EDIT') : 'BROWSE', ) @@ -28,6 +31,7 @@ {workspace.flash?.text ?? ''} + {#if pending}{pending}{/if} {#if workspace.fullscreen}full{/if} {#if workspace.vault?.unlocked}vault open{/if} {#if lines !== null}{lines}L{/if} @@ -46,6 +50,7 @@ .dirty { color: var(--light); } .msg { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--fg); } .msg.alarm { color: var(--alarm); } + .pending { flex: none; color: var(--light); font-family: var(--font-mono); } .flag { color: var(--light); letter-spacing: 0.18em; text-transform: uppercase; } .tools { display: none; } @@ -57,7 +62,7 @@ */ @media (max-width: 767px) { .status { padding: 0 6px; gap: 6px; overflow: visible; } - .hint, .file, .readout, .flag { display: none; } + .hint, .file, .readout, .flag, .pending { display: none; } .mode:not(.editing) { display: none; } .tools { display: flex; flex: 1; height: 100%; } diff --git a/web/src/lib/store.svelte.ts b/web/src/lib/store.svelte.ts index 834aa8c..6f112aa 100644 --- a/web/src/lib/store.svelte.ts +++ b/web/src/lib/store.svelte.ts @@ -652,6 +652,24 @@ class Workspace { await this.refresh() } + /** + * `n` / `N`: the next or previous search result after the cursor, opened + * beside the results so the next `n` goes on from there. A folder is only + * highlighted: going into it would end the search. Past the last result it + * starts again at the first, as vim's `wrapscan` does. + */ + async nextResult(delta: 1 | -1) { + if (!this.query) return this.say('no search — / starts one', 'error') + const count = this.nodes.length + if (count === 0) return this.say(`no results for "${this.query}"`, 'error') + const next = this.cursor + delta + if (next >= count) this.say('search hit BOTTOM, continuing at TOP') + else if (next < 0) this.say('search hit TOP, continuing at BOTTOM') + this.cursor = (next + count) % count + const node = this.nodes[this.cursor] + if (!isContainer(node)) await this.openNode(node.id) + } + move(delta: number) { if (this.nodes.length === 0) return this.cursor = Math.min(Math.max(this.cursor + delta, 0), this.nodes.length - 1)