mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
keys: n walks the search results, and a half-typed key shows
`n` / `N` move to the next / previous search result and open it beside the results, so the next `n` goes on from there; a folder is only highlighted, as going into it would end the search. Past the last result it starts again at the first and says so, as vim's `wrapscan` does. Clearing the search is left to `:noh` / `:clear`, as in vim, rather than to `n`. After `g`, `d`, `]` or `[` the status line shows the key waiting for its second half, like vim's `showcmd`, and forgets it after a second.
This commit is contained in:
1 parent
b3a60aa361
commit
c72eccf86b
5 files changed
+42
-7
No files matched your search
@@ -75,7 +75,7 @@ it; in the document pane it is that document. In the browse pane:
|
|||||||
| `f` | fullscreen the focused pane |
|
| `f` | fullscreen the focused pane |
|
||||||
| `Alt-h` / `Alt-l` | focus the list / the document, from the editor too |
|
| `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 |
|
| `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 |
|
| `Ctrl-D` / `Ctrl-U` | half a page down / up the open document |
|
||||||
| `]]` / `[[` | the next / previous heading of the open document, in the editor too |
|
| `]]` / `[[` | 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`) |
|
| `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 |
|
| `c` | capture a card into this space's inbox without leaving where you are |
|
||||||
| `Esc`, `q` | close the document |
|
| `Esc`, `q` | close the document |
|
||||||
| `h` | up one level in the tree |
|
| `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 |
|
| `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 |
|
| `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`) |
|
| `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 |
|
| `:` | 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 |
|
||||||
|
|||||||
+13
-2
@@ -44,6 +44,7 @@
|
|||||||
let importRoot = false
|
let importRoot = false
|
||||||
let shell = $state<HTMLDivElement>()
|
let shell = $state<HTMLDivElement>()
|
||||||
let overlayEl = $state<HTMLDivElement>()
|
let overlayEl = $state<HTMLDivElement>()
|
||||||
|
/** The first half of `gg`, `dd`, `]]` …, shown until the second comes. */
|
||||||
let pending = $state('')
|
let pending = $state('')
|
||||||
/** `F`: the pane whose links and buttons are wearing hints right now. */
|
/** `F`: the pane whose links and buttons are wearing hints right now. */
|
||||||
let hintRoot = $state<HTMLElement | null>(null)
|
let hintRoot = $state<HTMLElement | null>(null)
|
||||||
@@ -135,6 +136,13 @@
|
|||||||
return () => clearTimeout(timer)
|
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.
|
// Guard against losing a half-written entry to a stray refresh.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!workspace.dirty) return
|
if (!workspace.dirty) return
|
||||||
@@ -775,8 +783,11 @@
|
|||||||
})()
|
})()
|
||||||
break
|
break
|
||||||
case 'n':
|
case 'n':
|
||||||
|
case 'N':
|
||||||
|
// Through the results, as vim's `n` goes through the matches; the
|
||||||
|
// search itself stays until `:noh`.
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (workspace.query) void workspace.guard(() => workspace.search(''))
|
void workspace.guard(() => workspace.nextResult(key === 'n' ? 1 : -1))
|
||||||
break
|
break
|
||||||
case '/':
|
case '/':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -864,7 +875,7 @@
|
|||||||
{/key}
|
{/key}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<StatusBar />
|
<StatusBar {pending} />
|
||||||
|
|
||||||
{#if cmdline !== null}
|
{#if cmdline !== null}
|
||||||
<CommandLine initial={cmdline} global={cmdGlobal} close={() => ((cmdline = null), (cmdGlobal = false))} />
|
<CommandLine initial={cmdline} global={cmdGlobal} close={() => ((cmdline = null), (cmdGlobal = false))} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
['f', 'fullscreen the focused pane; again or Esc to leave'],
|
['f', 'fullscreen the focused pane; again or Esc to leave'],
|
||||||
['Alt-h / Alt-l', 'focus the list / the document, from the editor too'],
|
['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'],
|
['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'],
|
['Ctrl-D / Ctrl-U', 'half a page down / up the open document'],
|
||||||
[']] / [[', 'next / previous heading of the document — in the editor too'],
|
[']] / [[', 'next / previous heading of the document — in the editor too'],
|
||||||
['gO', 'the headings as a list: j/k, Enter jumps there'],
|
['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"],
|
['c', "capture a card into this space's inbox, without leaving here"],
|
||||||
['Esc · q', 'close the document'],
|
['Esc · q', 'close the document'],
|
||||||
['/', 'full-text search, inside the current space — Tab or Ctrl-N/P walk the results, Enter opens one'],
|
['/', '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'],
|
['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'],
|
['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)'],
|
['x · dd', 'delete what has focus — the row in the list, the document in its pane (asks y/N)'],
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import { workspace } from './store.svelte'
|
import { workspace } from './store.svelte'
|
||||||
import { isTouchDevice } from './util'
|
import { isTouchDevice } from './util'
|
||||||
|
|
||||||
|
/** A key waiting for its second half, like vim's `showcmd`. */
|
||||||
|
const { pending = '' }: { pending?: string } = $props()
|
||||||
|
|
||||||
const mode = $derived(
|
const mode = $derived(
|
||||||
workspace.editing ? (workspace.vimEnabled ? workspace.vimMode.toUpperCase() : 'EDIT') : 'BROWSE',
|
workspace.editing ? (workspace.vimEnabled ? workspace.vimMode.toUpperCase() : 'EDIT') : 'BROWSE',
|
||||||
)
|
)
|
||||||
@@ -28,6 +31,7 @@
|
|||||||
|
|
||||||
<span class="msg" class:alarm={workspace.flash?.kind === 'error'}>{workspace.flash?.text ?? ''}</span>
|
<span class="msg" class:alarm={workspace.flash?.kind === 'error'}>{workspace.flash?.text ?? ''}</span>
|
||||||
|
|
||||||
|
{#if pending}<span class="pending" aria-live="polite">{pending}</span>{/if}
|
||||||
{#if workspace.fullscreen}<span class="flag">full</span>{/if}
|
{#if workspace.fullscreen}<span class="flag">full</span>{/if}
|
||||||
{#if workspace.vault?.unlocked}<span class="flag vault" title="the vault is open — :lock closes it">vault open</span>{/if}
|
{#if workspace.vault?.unlocked}<span class="flag vault" title="the vault is open — :lock closes it">vault open</span>{/if}
|
||||||
{#if lines !== null}<span class="readout">{lines}<span class="unit">L</span></span>{/if}
|
{#if lines !== null}<span class="readout">{lines}<span class="unit">L</span></span>{/if}
|
||||||
@@ -46,6 +50,7 @@
|
|||||||
.dirty { color: var(--light); }
|
.dirty { color: var(--light); }
|
||||||
.msg { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
|
.msg { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
|
||||||
.msg.alarm { color: var(--alarm); }
|
.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; }
|
.flag { color: var(--light); letter-spacing: 0.18em; text-transform: uppercase; }
|
||||||
|
|
||||||
.tools { display: none; }
|
.tools { display: none; }
|
||||||
@@ -57,7 +62,7 @@
|
|||||||
*/
|
*/
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.status { padding: 0 6px; gap: 6px; overflow: visible; }
|
.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; }
|
.mode:not(.editing) { display: none; }
|
||||||
.tools { display: flex; flex: 1; height: 100%; }
|
.tools { display: flex; flex: 1; height: 100%; }
|
||||||
|
|
||||||
|
|||||||
@@ -652,6 +652,24 @@ class Workspace {
|
|||||||
await this.refresh()
|
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) {
|
move(delta: number) {
|
||||||
if (this.nodes.length === 0) return
|
if (this.nodes.length === 0) return
|
||||||
this.cursor = Math.min(Math.max(this.cursor + delta, 0), this.nodes.length - 1)
|
this.cursor = Math.min(Math.max(this.cursor + delta, 0), this.nodes.length - 1)
|
||||||
|
|||||||
Reference in new issue
Block a user