mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
search: Esc on the line gives it up, Esc in the list lets go of it
Typing after / only tries a search: Esc, or back, puts the list back as it was before the line opened, and only Enter keeps the results. In the list, Esc lets go of one thing at a time, so after the document it clears the search, with the cursor kept on the row it was on; back on a phone does the same before going up a folder. A late answer to an older search no longer lands over a newer one, and closing the last document puts the folder back in the address bar.
This commit is contained in:
1 parent
a27d5de9c7
commit
4542b9692e
9 files changed
+60
-16
No files matched your search
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `/` | 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 — :noh clears the search |
|
| `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) |
|
| `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 |
|
| `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 |
|
| `gh` | the open document's earlier versions, as diffs — Enter brings one back |
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
line, and `Enter` opens the one lit and keeps the results in the list. `Esc`
|
||||||
previous result beside the list, and `:noh` clears the search.
|
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
|
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
|
are listed after the documents, under `cards`, with the board and list each is
|
||||||
|
|||||||
@@ -121,6 +121,26 @@
|
|||||||
void tick().then(() => list?.querySelector('.current')?.scrollIntoView({ block: 'nearest' }))
|
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`.
|
// A search is live: the list filters as you type, like `incsearch`.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!isSearch) return
|
if (!isSearch) return
|
||||||
@@ -174,6 +194,7 @@
|
|||||||
const line = value
|
const line = value
|
||||||
// Closing takes the line's props with it, so what it was is read first.
|
// Closing takes the line's props with it, so what it was is read first.
|
||||||
const [jumping, searching, place] = [jump, isSearch, places[lit]]
|
const [jumping, searching, place] = [jump, isSearch, places[lit]]
|
||||||
|
entered = true
|
||||||
remember(historyKey, line)
|
remember(historyKey, line)
|
||||||
close()
|
close()
|
||||||
if (jumping) return go(place)
|
if (jumping) return go(place)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<div class="filter">
|
<div class="filter">
|
||||||
<span class="muted">/</span><span class="query">{workspace.tree.query}</span>
|
<span class="muted">/</span><span class="query">{workspace.tree.query}</span>
|
||||||
{#if workspace.tree.globalSearch}<span class="muted scope">all spaces</span>{/if}
|
{#if workspace.tree.globalSearch}<span class="muted scope">all spaces</span>{/if}
|
||||||
<button title="clear search (n)" onclick={() => workspace.guard(() => workspace.tree.search(''))}>esc</button>
|
<button title="clear the search (Esc)" onclick={() => workspace.guard(() => workspace.tree.clearSearch())}>esc</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -431,16 +431,19 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
|||||||
if (workspace.open) void runCommand('q')
|
if (workspace.open) void runCommand('q')
|
||||||
else workspace.tree.unmarkAll()
|
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: () => {
|
escape: () => {
|
||||||
if (workspace.tree.marked.length) return workspace.tree.unmarkAll()
|
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')
|
if (workspace.open) void runCommand('q')
|
||||||
},
|
},
|
||||||
delete: () => void runCommand('d'),
|
delete: () => void runCommand('d'),
|
||||||
// A delete goes to the trash, so it can be undone the way vim undoes.
|
// A delete goes to the trash, so it can be undone the way vim undoes.
|
||||||
undo: () => guard(() => workspace.tree.undoDelete()),
|
undo: () => guard(() => workspace.tree.undoDelete()),
|
||||||
// Through the results, as vim's `n` goes through the matches; the
|
// 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)),
|
next: () => guard(() => workspace.tree.nextResult(1)),
|
||||||
previous: () => guard(() => workspace.tree.nextResult(-1)),
|
previous: () => guard(() => workspace.tree.nextResult(-1)),
|
||||||
search: () => line('/'),
|
search: () => line('/'),
|
||||||
|
|||||||
@@ -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: ['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: '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: ['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: ['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: ['/'], 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 — :noh clears the search' },
|
{ 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: ['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: ['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 },
|
{ context: 'tree', keys: ['u'], action: 'undo', help: 'undo the last delete: it comes back out of the trash — :trash has the rest', essential: true },
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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.
|
* nothing left to leave, and the app itself may go.
|
||||||
*/
|
*/
|
||||||
export function back(): boolean {
|
export function back(): boolean {
|
||||||
@@ -177,6 +177,7 @@ function leaving(): (() => unknown) | null {
|
|||||||
if (workspace.fullscreen) return () => (workspace.fullscreen = false)
|
if (workspace.fullscreen) return () => (workspace.fullscreen = false)
|
||||||
if (workspace.focus !== 'list' && workspace.focus !== 'node') return () => runCommand('close')
|
if (workspace.focus !== 'list' && workspace.focus !== 'node') return () => runCommand('close')
|
||||||
if (workspace.open && workspace.pane === 'document') return () => runCommand('q')
|
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())
|
if (tree.path.length > 1) return () => workspace.guard(() => tree.up())
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -180,6 +180,8 @@ export class Tiles {
|
|||||||
this.current.clear()
|
this.current.clear()
|
||||||
this.prune()
|
this.prune()
|
||||||
if (!this.current.empty) this.focusOn(this.focused)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ export class Tree {
|
|||||||
const folder = await api.getNode(id)
|
const folder = await api.getNode(id)
|
||||||
// Walking into another space by a link leaves this one where it was.
|
// 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()
|
if (this.spaceId !== null && folder.path[0]?.id !== this.spaceId) this.#rememberSpot()
|
||||||
|
this.#reading++
|
||||||
this.parentId = id
|
this.parentId = id
|
||||||
this.parentSealed = folder.sealed
|
this.parentSealed = folder.sealed
|
||||||
this.query = ''
|
this.query = ''
|
||||||
@@ -259,19 +260,27 @@ export class Tree {
|
|||||||
await this.goTo(parent.id, from.id)
|
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. */
|
/** `keep` follows a node that just arrived, instead of holding the cursor. */
|
||||||
async refresh(keep?: number) {
|
async refresh(keep?: number) {
|
||||||
const keepId = keep ?? this.selected?.id
|
const keepId = keep ?? this.selected?.id
|
||||||
|
const reading = ++this.#reading
|
||||||
if (this.query) {
|
if (this.query) {
|
||||||
const space = this.searchSpace
|
const space = this.searchSpace
|
||||||
// A note is in no space, so every search looks through them all.
|
// 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.listNodes({ q: this.query, space }),
|
||||||
api.searchCards(this.query, space),
|
api.searchCards(this.query, space),
|
||||||
api.scratch(this.query),
|
api.scratch(this.query),
|
||||||
])
|
])
|
||||||
|
if (reading !== this.#reading) return
|
||||||
|
;[this.nodes, this.foundCards, this.foundNotes] = found
|
||||||
} else {
|
} 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.foundCards = []
|
||||||
this.foundNotes = []
|
this.foundNotes = []
|
||||||
}
|
}
|
||||||
@@ -312,11 +321,17 @@ export class Tree {
|
|||||||
return this.globalSearch ? undefined : (this.spaceId ?? undefined)
|
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.query = query
|
||||||
this.globalSearch = global && query !== ''
|
this.globalSearch = global && query !== ''
|
||||||
this.cursor = 0
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user