diff --git a/docs/wiki/Commands.md b/docs/wiki/Commands.md
index fc5f005..0ac34b7 100644
--- a/docs/wiki/Commands.md
+++ b/docs/wiki/Commands.md
@@ -51,9 +51,9 @@ arguments and `↑` / `↓` recall earlier lines.
| `Ctrl-P` | go to anything by its name, in every space — words before the last narrow the path, as in proj/ingest; Tab or Ctrl-N/P walk, Enter goes |
| `b` | the board, on the plan screen: this space's, or the last one shown — 1 … 9 there shows another's |
| `c` | capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry |
-| `Esc · q` | close the document, and its tile when there are several — Esc first puts down what m picked up |
-| `/` | full-text search, inside the current space and its text files — Tab takes it to every space, Ctrl-N/P walk the results, Enter opens one |
-| `n / N` | next / previous search result, opened beside the results — :noh clears the search |
+| `Esc · q` | close the document, and its tile when there are several — Esc first puts down what m picked up, and in the list lets go of a search |
+| `/` | full-text search, inside the current space and its text files — Tab takes it to every space, Ctrl-N/P walk the results, Enter opens one, Esc gives up |
+| `n / N` | next / previous search result, opened beside the results — Esc or :noh clears the search |
| `x · dd` | delete what has focus — the row in the list, the document in its pane — into the trash (asks y/N) |
| `u` | undo the last delete: it comes back out of the trash — :trash has the rest |
| `gh` | the open document's earlier versions, as diffs — Enter brings one back |
diff --git a/docs/wiki/Files-and-Search.md b/docs/wiki/Files-and-Search.md
index d948c05..20812c1 100644
--- a/docs/wiki/Files-and-Search.md
+++ b/docs/wiki/Files-and-Search.md
@@ -58,8 +58,10 @@ space, and back (`:search!` does the same from the command line).
Results appear as you type, ranked by relevance, each with the space and
folder it is in. `Ctrl-N` / `Ctrl-P` walk them without leaving the
-line, and `Enter` opens the one lit. Afterwards `n` and `N` open the next and
-previous result beside the list, and `:noh` clears the search.
+line, and `Enter` opens the one lit and keeps the results in the list. `Esc`
+on the line gives the search up, and the list is as it was before `/`.
+Afterwards `n` and `N` open the next and previous result beside the list, and
+`Esc` in the list (or `:noh`, or back on a phone) clears the search.
Cards are searched too, by their title, description and checklist steps. They
are listed after the documents, under `cards`, with the board and list each is
diff --git a/web/src/lib/CommandLine.svelte b/web/src/lib/CommandLine.svelte
index f3b941a..9156127 100644
--- a/web/src/lib/CommandLine.svelte
+++ b/web/src/lib/CommandLine.svelte
@@ -121,6 +121,26 @@
void tick().then(() => list?.querySelector('.current')?.scrollIntoView({ block: 'nearest' }))
})
+ /*
+ * What the list showed before the search line opened. Esc, or anything else
+ * that closes the line without Enter, puts it back, as vim puts the cursor
+ * back when a `/` is abandoned: typing a search only tries it.
+ */
+ const before = untrack(() => ({
+ query: workspace.tree.query,
+ global: workspace.tree.globalSearch,
+ selected: workspace.tree.selected?.id,
+ }))
+ let entered = false
+
+ $effect(() => () => {
+ if (entered) return
+ const { tree } = workspace
+ if (tree.query !== before.query || tree.globalSearch !== before.global) {
+ void workspace.guard(() => tree.search(before.query, before.global, before.selected))
+ }
+ })
+
// A search is live: the list filters as you type, like `incsearch`.
$effect(() => {
if (!isSearch) return
@@ -174,6 +194,7 @@
const line = value
// Closing takes the line's props with it, so what it was is read first.
const [jumping, searching, place] = [jump, isSearch, places[lit]]
+ entered = true
remember(historyKey, line)
close()
if (jumping) return go(place)
diff --git a/web/src/lib/Sidebar.svelte b/web/src/lib/Sidebar.svelte
index d2e2099..27013d7 100644
--- a/web/src/lib/Sidebar.svelte
+++ b/web/src/lib/Sidebar.svelte
@@ -101,7 +101,7 @@
{/if}
diff --git a/web/src/lib/actions.ts b/web/src/lib/actions.ts
index 3ecb96c..a8541fd 100644
--- a/web/src/lib/actions.ts
+++ b/web/src/lib/actions.ts
@@ -431,16 +431,19 @@ export const actions: { [C in Context]: Record, Run> } = {
if (workspace.open) void runCommand('q')
else workspace.tree.unmarkAll()
},
- // Esc lets go of one thing at a time: what is carried, then the document.
+ // Esc lets go of one thing at a time: what is carried, then what has the
+ // keys — the document read, or the search the list shows — then the rest.
escape: () => {
if (workspace.tree.marked.length) return workspace.tree.unmarkAll()
+ if (workspace.open && workspace.pane === 'document') return void runCommand('q')
+ if (workspace.tree.query) return guard(() => workspace.tree.clearSearch())
if (workspace.open) void runCommand('q')
},
delete: () => void runCommand('d'),
// A delete goes to the trash, so it can be undone the way vim undoes.
undo: () => guard(() => workspace.tree.undoDelete()),
// Through the results, as vim's `n` goes through the matches; the
- // search itself stays until `:noh`.
+ // search itself stays until Esc or `:noh`.
next: () => guard(() => workspace.tree.nextResult(1)),
previous: () => guard(() => workspace.tree.nextResult(-1)),
search: () => line('/'),
diff --git a/web/src/lib/keymap.ts b/web/src/lib/keymap.ts
index d6cd112..cb0d29d 100644
--- a/web/src/lib/keymap.ts
+++ b/web/src/lib/keymap.ts
@@ -117,10 +117,10 @@ export const bindings = [
{ context: 'global', keys: ['Ctrl-p'], action: 'find', label: 'Ctrl-P', help: 'go to anything by its name, in every space — words before the last narrow the path, as in proj/ingest; Tab or Ctrl-N/P walk, Enter goes', essential: true },
{ context: 'global', keys: ['b'], action: 'board', help: "the board, on the plan screen: this space's, or the last one shown — 1 … 9 there shows another's" },
{ context: 'tree', keys: ['c'], action: 'capture', help: "capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry", essential: true },
- { context: 'tree', keys: ['q'], action: 'close', label: 'Esc · q', help: 'close the document, and its tile when there are several — Esc first puts down what m picked up', essential: true },
+ { context: 'tree', keys: ['q'], action: 'close', label: 'Esc · q', help: 'close the document, and its tile when there are several — Esc first puts down what m picked up, and in the list lets go of a search', essential: true },
{ context: 'tree', keys: ['Escape'], action: 'escape' },
- { context: 'tree', keys: ['/'], action: 'search', help: 'full-text search, inside the current space and its text files — Tab takes it to every space, Ctrl-N/P walk the results, Enter opens one', essential: true },
- { context: 'tree', keys: ['n'], action: 'next', label: 'n / N', help: 'next / previous search result, opened beside the results — :noh clears the search' },
+ { context: 'tree', keys: ['/'], action: 'search', help: 'full-text search, inside the current space and its text files — Tab takes it to every space, Ctrl-N/P walk the results, Enter opens one, Esc gives up', essential: true },
+ { context: 'tree', keys: ['n'], action: 'next', label: 'n / N', help: 'next / previous search result, opened beside the results — Esc or :noh clears the search' },
{ context: 'tree', keys: ['N'], action: 'previous' },
{ context: 'tree', keys: ['x', 'dd'], action: 'delete', help: 'delete what has focus — the row in the list, the document in its pane — into the trash (asks y/N)', essential: true },
{ context: 'tree', keys: ['u'], action: 'undo', help: 'undo the last delete: it comes back out of the trash — :trash has the rest', essential: true },
diff --git a/web/src/lib/modes.ts b/web/src/lib/modes.ts
index b4a68db..3993a8c 100644
--- a/web/src/lib/modes.ts
+++ b/web/src/lib/modes.ts
@@ -155,7 +155,7 @@ export function claim(event: KeyboardEvent): boolean {
/*
* The phone's back gesture, or its back button: one thing let go of, the way
* Esc and q let go of it on a keyboard — a question, the line, a sheet, the
- * drawer, the editor, a tile — and then up a folder. False when there is
+ * drawer, the editor, a tile, a search — and then up a folder. False when there is
* nothing left to leave, and the app itself may go.
*/
export function back(): boolean {
@@ -177,6 +177,7 @@ function leaving(): (() => unknown) | null {
if (workspace.fullscreen) return () => (workspace.fullscreen = false)
if (workspace.focus !== 'list' && workspace.focus !== 'node') return () => runCommand('close')
if (workspace.open && workspace.pane === 'document') return () => runCommand('q')
+ if (tree.query) return () => workspace.guard(() => tree.clearSearch())
if (tree.path.length > 1) return () => workspace.guard(() => tree.up())
return null
}
diff --git a/web/src/lib/state/tiles.svelte.ts b/web/src/lib/state/tiles.svelte.ts
index ab3a658..69f8c4b 100644
--- a/web/src/lib/state/tiles.svelte.ts
+++ b/web/src/lib/state/tiles.svelte.ts
@@ -180,6 +180,8 @@ export class Tiles {
this.current.clear()
this.prune()
if (!this.current.empty) this.focusOn(this.focused)
+ // Nothing open: the address is the folder's again.
+ else this.ws.tree.syncUrl(this.ws.tree.path)
}
/**
diff --git a/web/src/lib/state/tree.svelte.ts b/web/src/lib/state/tree.svelte.ts
index 46a6145..973fb36 100644
--- a/web/src/lib/state/tree.svelte.ts
+++ b/web/src/lib/state/tree.svelte.ts
@@ -239,6 +239,7 @@ export class Tree {
const folder = await api.getNode(id)
// Walking into another space by a link leaves this one where it was.
if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot()
+ this.#reading++
this.parentId = id
this.parentSealed = folder.sealed
this.query = ''
@@ -259,19 +260,27 @@ export class Tree {
await this.goTo(parent.id, from.id)
}
+ /** Which `refresh` is the latest: an older one answering late is dropped. */
+ #reading = 0
+
/** `keep` follows a node that just arrived, instead of holding the cursor. */
async refresh(keep?: number) {
const keepId = keep ?? this.selected?.id
+ const reading = ++this.#reading
if (this.query) {
const space = this.searchSpace
// A note is in no space, so every search looks through them all.
- ;[this.nodes, this.foundCards, this.foundNotes] = await Promise.all([
+ const found = await Promise.all([
api.listNodes({ q: this.query, space }),
api.searchCards(this.query, space),
api.scratch(this.query),
])
+ if (reading !== this.#reading) return
+ ;[this.nodes, this.foundCards, this.foundNotes] = found
} else {
- this.nodes = await this.#withFiles(await api.listNodes({ parent: this.parentId ?? undefined }))
+ const nodes = await this.#withFiles(await api.listNodes({ parent: this.parentId ?? undefined }))
+ if (reading !== this.#reading) return
+ this.nodes = nodes
this.foundCards = []
this.foundNotes = []
}
@@ -312,11 +321,17 @@ export class Tree {
return this.globalSearch ? undefined : (this.spaceId ?? undefined)
}
- async search(query: string, global = this.globalSearch) {
+ /** `keep`: the node the cursor stays on, when the list it lands in has it. */
+ async search(query: string, global = this.globalSearch, keep?: number) {
this.query = query
this.globalSearch = global && query !== ''
this.cursor = 0
- await this.refresh()
+ await this.refresh(keep)
+ }
+
+ /** Esc, or back: the search let go of, the folder shown again with the cursor where it was in the results. */
+ async clearSearch() {
+ await this.search('', false, this.selected?.id)
}
/**