mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 00:47:01 +01:00
navigation: the list by name outside the diary, Ctrl-P goes to anything by its name, new things land beside what they belong to
The list is folders first and then by name in every space but the diary, which stays a journal by day; a document's row elsewhere shows when it was last edited. Ctrl-P is a jump by name across every space, served by GET /api/jump: the last word is looked for in the name, the ones before along the path. / is the one full-text search, and Tab on its line takes it to every space and back. A card's notes are made in the folder being browsed when that is in the card's space, and a missing [[link]]'s document beside the one linking to it. o in a space with templates opens the line as :new with them listed.
This commit is contained in:
1 parent
c70bfdd7be
commit
9363a0024d
21 files changed
+297
-96
No files matched your search
+8
-8
@@ -42,7 +42,7 @@
|
||||
const shareKey = shareToken ? decodeURIComponent(location.hash.slice(1)) : ''
|
||||
|
||||
let cmdline = $state<string | null>(null)
|
||||
let cmdGlobal = $state(false)
|
||||
let cmdJump = $state(false)
|
||||
let fileInput = $state<HTMLInputElement>()
|
||||
let treeInput = $state<HTMLInputElement>()
|
||||
/** Whether the files being picked go into the vault — `:upload seal`. */
|
||||
@@ -98,13 +98,13 @@
|
||||
}
|
||||
hooks.focusList = () => shell?.focus()
|
||||
hooks.openCommandLine = (initial) => {
|
||||
cmdGlobal = false
|
||||
cmdJump = false
|
||||
cmdline = initial
|
||||
}
|
||||
hooks.openOverview = () => void openOverview()
|
||||
hooks.openGlobalSearch = () => {
|
||||
cmdGlobal = true
|
||||
cmdline = '/'
|
||||
hooks.openJump = () => {
|
||||
cmdJump = true
|
||||
cmdline = ''
|
||||
}
|
||||
hooks.closeCommandLine = () => {
|
||||
if (cmdline === null) return false
|
||||
@@ -249,8 +249,8 @@
|
||||
[keysOf('global', 'plan'), 'the plan: board, agenda, calendar'],
|
||||
[[...keysOf('global', 'board'), ...keysOf('global', 'agenda'), ...keysOf('global', 'calendar')], 'one of them, on the plan'],
|
||||
[keysOf('tree', 'capture'), 'capture a card to the inbox'],
|
||||
[keysOf('global', 'find'), 'find in every space'],
|
||||
[keysOf('tree', 'search'), 'search this space'],
|
||||
[keysOf('global', 'find'), 'go to anything by its name'],
|
||||
[keysOf('tree', 'search'), 'search this space, Tab every space'],
|
||||
[keysOf('global', 'help'), 'help'],
|
||||
]
|
||||
|
||||
@@ -355,7 +355,7 @@
|
||||
<StatusBar {pending} />
|
||||
|
||||
{#if cmdline !== null}
|
||||
<CommandLine initial={cmdline} global={cmdGlobal} close={() => ((cmdline = null), (cmdGlobal = false))} />
|
||||
<CommandLine initial={cmdline} jump={cmdJump} close={() => ((cmdline = null), (cmdJump = false))} />
|
||||
{/if}
|
||||
|
||||
{#if workspace.scratch.open}
|
||||
|
||||
@@ -28,13 +28,14 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { tick, untrack } from 'svelte'
|
||||
import { api, type NodeSummary } from './api'
|
||||
import { argCompletions, completions, readingDocument, runCommand, hooks } from './commands'
|
||||
import { workspace } from './store.svelte'
|
||||
|
||||
interface Props {
|
||||
initial: string
|
||||
/** A `/` search across every space rather than this one — Ctrl-P. */
|
||||
global?: boolean
|
||||
/** Ctrl-P: a jump to a node by its name, in every space, rather than a command. */
|
||||
jump?: boolean
|
||||
close: () => void
|
||||
}
|
||||
|
||||
@@ -46,12 +47,18 @@
|
||||
help?: string
|
||||
}
|
||||
|
||||
const { initial, global = false, close }: Props = $props()
|
||||
const { initial, jump = false, close }: Props = $props()
|
||||
|
||||
let value = $state(untrack(() => initial))
|
||||
let input = $state<HTMLInputElement>()
|
||||
let list = $state<HTMLUListElement>()
|
||||
const isSearch = $derived(value.startsWith('/'))
|
||||
const isSearch = $derived(!jump && value.startsWith('/'))
|
||||
/** A search across every space rather than this one; Tab switches. */
|
||||
let global = $state(false)
|
||||
|
||||
/* What a jump offers for what is typed, and the one lit. */
|
||||
let places = $state<NodeSummary[]>([])
|
||||
let lit = $state(0)
|
||||
|
||||
/*
|
||||
* A capture line — `c` opens one — writes a thought down somewhere without
|
||||
@@ -70,7 +77,7 @@
|
||||
value = value.replace(/^:\S+/, `:${next}`)
|
||||
return true
|
||||
}
|
||||
const historyKey = $derived(isSearch ? 'diary:history:search' : 'diary:history:cmd')
|
||||
const historyKey = $derived(jump ? 'diary:history:jump' : isSearch ? 'diary:history:search' : 'diary:history:cmd')
|
||||
|
||||
/*
|
||||
* Where the keys were before the line took them: the line goes away under
|
||||
@@ -98,6 +105,7 @@
|
||||
}
|
||||
|
||||
const shown = $derived.by((): Item[] => {
|
||||
if (jump) return places.map((node) => ({ value: String(node.id), label: node.name || node.slug, args: node.location }))
|
||||
if (cycle) return cycle.items
|
||||
if (isSearch || value.replace(/^:/, '').length === 0) return []
|
||||
return candidates(value).slice(0, 8)
|
||||
@@ -109,7 +117,7 @@
|
||||
|
||||
// The match Tab is on stays in sight in a list longer than the box.
|
||||
$effect(() => {
|
||||
if (cycle?.at === undefined) return
|
||||
if ((jump ? lit : cycle?.at) === undefined) return
|
||||
void tick().then(() => list?.querySelector('.current')?.scrollIntoView({ block: 'nearest' }))
|
||||
})
|
||||
|
||||
@@ -117,10 +125,35 @@
|
||||
$effect(() => {
|
||||
if (!isSearch) return
|
||||
const query = value.slice(1)
|
||||
const timer = setTimeout(() => void workspace.guard(() => workspace.tree.search(query, global)), 120)
|
||||
const everywhere = global
|
||||
const timer = setTimeout(() => void workspace.guard(() => workspace.tree.search(query, everywhere)), 120)
|
||||
return () => clearTimeout(timer)
|
||||
})
|
||||
|
||||
// So is a jump, with the places it offers under the line.
|
||||
$effect(() => {
|
||||
if (!jump) return
|
||||
const query = value
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
void workspace.guard(async () => {
|
||||
const found = await api.jump(query)
|
||||
if (value !== query) return
|
||||
places = found
|
||||
lit = 0
|
||||
}),
|
||||
120,
|
||||
)
|
||||
return () => clearTimeout(timer)
|
||||
})
|
||||
|
||||
/** Enter or a click on a jump: the place opened, a folder by going in. */
|
||||
async function go(node: NodeSummary | undefined) {
|
||||
if (!node) return
|
||||
await workspace.guard(() => workspace.tree.openPath(node.location ? `${node.location}/${node.slug}` : node.slug))
|
||||
await giveBackFocus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand the keys back once the line is gone, unless what ran took them
|
||||
* somewhere itself — into the editor, a prompt, an overlay.
|
||||
@@ -139,9 +172,12 @@
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault()
|
||||
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]]
|
||||
remember(historyKey, line)
|
||||
close()
|
||||
if (isSearch) {
|
||||
if (jumping) return go(place)
|
||||
if (searching) {
|
||||
// Enter goes to the highlighted result, once the results are for what
|
||||
// was typed rather than for the line a keystroke ago.
|
||||
const query = line.slice(1)
|
||||
@@ -199,10 +235,15 @@
|
||||
event.stopPropagation()
|
||||
close()
|
||||
void giveBackFocus()
|
||||
} else if (event.key === 'Tab' && isSearch) {
|
||||
// Nothing to complete in a search: Tab walks the results instead.
|
||||
} else if (jump && (event.key === 'Tab' || (ctrl && (event.key === 'n' || event.key === 'p')))) {
|
||||
// The places, walked without leaving the line; Enter goes to the one lit.
|
||||
event.preventDefault()
|
||||
workspace.tree.move(event.shiftKey ? -1 : 1)
|
||||
const count = places.length
|
||||
if (count) lit = (lit + (event.shiftKey || event.key === 'p' ? -1 : 1) + count) % count
|
||||
} else if (event.key === 'Tab' && isSearch) {
|
||||
// Nothing to complete in a search: Tab takes it to every space, or back.
|
||||
event.preventDefault()
|
||||
global = !global
|
||||
} else if (event.key === 'Tab') {
|
||||
event.preventDefault()
|
||||
if (!retarget(event.shiftKey ? -1 : 1)) complete(event.shiftKey ? -1 : 1)
|
||||
@@ -231,8 +272,8 @@
|
||||
{#each shown as item, index (item.value)}
|
||||
<li>
|
||||
<button
|
||||
class:current={cycle?.at === index}
|
||||
onclick={() => ((value = item.value), (cycle = null), input?.focus())}
|
||||
class:current={jump ? lit === index : cycle?.at === index}
|
||||
onclick={() => (jump ? (close(), void go(places[index])) : ((value = item.value), (cycle = null), input?.focus()))}
|
||||
>
|
||||
<span class="name">{item.label}</span>
|
||||
{#if item.args}<span class="muted">{item.args}</span>{/if}
|
||||
@@ -244,7 +285,8 @@
|
||||
{/if}
|
||||
|
||||
<form onsubmit={submit}>
|
||||
{#if isSearch}<span class="scope label">{global ? 'all spaces' : (workspace.tree.space?.slug ?? '')}</span>{/if}
|
||||
{#if isSearch}<span class="scope label" title="Tab: this space · every space">{global ? 'all spaces' : (workspace.tree.space?.slug ?? '')} <span class="kbd">tab</span></span>{/if}
|
||||
{#if jump}<span class="scope label">go to</span>{/if}
|
||||
{#if capture}<span class="scope label" title="Tab: card · note · today">→ {targetNames[capture]} <span class="kbd">tab</span></span>{/if}
|
||||
<input bind:this={input} bind:value {onkeydown} {oninput} spellcheck="false" autocomplete="off" />
|
||||
</form>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
desk: true,
|
||||
},
|
||||
{ icon: 'search', label: 'Search this space', keys: keysOf('tree', 'search'), run: () => hooks.openCommandLine('/') },
|
||||
{ icon: 'search-all', label: 'Search every space', keys: keysOf('global', 'find'), run: () => hooks.openGlobalSearch() },
|
||||
{ icon: 'search-all', label: 'Go to anything by its name', keys: keysOf('global', 'find'), run: () => hooks.openJump() },
|
||||
],
|
||||
[cmd, settings, help],
|
||||
]
|
||||
|
||||
@@ -20,15 +20,19 @@
|
||||
return node.name.trim() || node.excerpt.trim() || node.slug
|
||||
}
|
||||
|
||||
/* The diary is a journal, listed by day; anywhere else the list is by name. */
|
||||
const journal = $derived(workspace.tree.space?.role === 'diary')
|
||||
|
||||
/*
|
||||
* A folder is listed by what is in it, a document by when it was written —
|
||||
* and how many files it has, folded or not — a file by its size.
|
||||
* A folder is listed by what is in it, a document by when it was written in
|
||||
* the diary and when it was last edited elsewhere — and how many files it
|
||||
* has, folded or not — a file by its size.
|
||||
*/
|
||||
const aside = (node: NodeSummary) =>
|
||||
node.file
|
||||
? formatBytes(node.file.size)
|
||||
: node.kind === 'document'
|
||||
? `${node.child_count > 0 ? `+${node.child_count} · ` : ''}${formatTime(node.created_at)}`
|
||||
? `${node.child_count > 0 ? `+${node.child_count} · ` : ''}${journal ? formatTime(node.created_at) : relative(node.updated_at)}`
|
||||
: `${node.child_count}`
|
||||
|
||||
/* The row a day heading compares with: the one above, past a document's files. */
|
||||
@@ -114,9 +118,9 @@
|
||||
{#each workspace.tree.nodes as node, index (node.id)}
|
||||
{@const previous = above(index)}
|
||||
{@const nested = workspace.tree.isNested(node)}
|
||||
<!-- Day headings only make sense in a list that is a journal: documents,
|
||||
in date order, not a search and not a folder listing. -->
|
||||
{#if !workspace.tree.query && !nested && node.kind === 'document' && (!previous || previous.kind !== 'document' || formatDay(previous.created_at) !== formatDay(node.created_at))}
|
||||
<!-- Day headings only make sense in a list that is a journal: the
|
||||
diary's documents, in date order, not a search. -->
|
||||
{#if journal && !workspace.tree.query && !nested && node.kind === 'document' && (!previous || previous.kind !== 'document' || formatDay(previous.created_at) !== formatDay(node.created_at))}
|
||||
<li class="daybreak label rule">
|
||||
<span><span class="day">{formatDay(node.created_at)}</span> {formatWeekday(node.created_at)}</span>
|
||||
</li>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* share an action and differ in direction. Returning `false` lets the key
|
||||
* on to the next context down, as if it had not been bound here.
|
||||
*/
|
||||
import { cycleTile, focusTile, followLink, hooks, openInfo, runCommand } from './commands'
|
||||
import { cycleTile, focusTile, followLink, hooks, newDocument, openInfo, runCommand } from './commands'
|
||||
import { bindings, type Context } from './keymap'
|
||||
import { deeperHelp, focusHelpFilter, scrollHelp } from './Help.svelte'
|
||||
import { actSetting, moveSettings, removeSetting, turnSettings, openSettings } from './Settings.svelte'
|
||||
@@ -413,7 +413,7 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
markAll: () => workspace.tree.markAll(),
|
||||
files: () => guard(() => workspace.tree.toggleFiles()),
|
||||
drop: () => guard(() => workspace.tree.drop()),
|
||||
new: () => void runCommand('new'),
|
||||
new: () => guard(newDocument),
|
||||
// Capture: a thought goes on this space's inbox without leaving here.
|
||||
// Reading a document, `:card` makes it about that document.
|
||||
capture: () => line(':card '),
|
||||
@@ -534,9 +534,8 @@ export const actions: { [C in Context]: Record<ActionOf<C>, Run> } = {
|
||||
},
|
||||
|
||||
global: {
|
||||
// Ctrl-P: find anything, in any space — the one search that is not
|
||||
// confined to where you are.
|
||||
find: () => hooks.openGlobalSearch(),
|
||||
// Ctrl-P: go to anything by its name, in any space.
|
||||
find: () => hooks.openJump(),
|
||||
// Ctrl-D / Ctrl-U page the open document, as they do in a pager.
|
||||
halfDown: () => (workspace.open && !workspace.editing ? void hooks.scrollReader('halfdown') : false),
|
||||
halfUp: () => (workspace.open && !workspace.editing ? void hooks.scrollReader('halfup') : false),
|
||||
|
||||
@@ -458,6 +458,8 @@ export const api = {
|
||||
|
||||
/** `work/einarbeiten/01-was-ist-eebus` — the node behind a `/n/...` address. */
|
||||
resolvePath: (path: string) => request<WorkspaceNode>(`/resolve?path=${encodeURIComponent(path)}`),
|
||||
/** Ctrl-P: the nodes called what was typed, in every space, with where each is. */
|
||||
jump: (q: string) => request<NodeSummary[]>(`/jump?q=${encodeURIComponent(q)}`),
|
||||
|
||||
/**
|
||||
* Upload a folder into a node. The relative path is passed as each part's
|
||||
|
||||
+17
-3
@@ -44,8 +44,8 @@ export const hooks = {
|
||||
overviewColumns: (): number => 1,
|
||||
openOverview: () => {},
|
||||
openCommandLine: (_initial: string) => {},
|
||||
/** Ctrl-P: a search across every space. */
|
||||
openGlobalSearch: () => {},
|
||||
/** Ctrl-P: a jump to a node by its name, in every space. */
|
||||
openJump: () => {},
|
||||
/** Esc with the line open but the keyboard elsewhere: the line away; false when it is not open. */
|
||||
closeCommandLine: (): boolean => false,
|
||||
/** `F`: letters over the links and buttons inside `root`. */
|
||||
@@ -271,6 +271,20 @@ function templateNames(): string[] {
|
||||
return templateCache.names
|
||||
}
|
||||
|
||||
/**
|
||||
* `o`: a new document here. When the space has templates the line comes up
|
||||
* as `:new ` with them listed — Tab picks one, Enter on nothing is a blank
|
||||
* document. The diary starts from `templates/day` by itself, so it is not
|
||||
* asked.
|
||||
*/
|
||||
export async function newDocument() {
|
||||
const space = workspace.tree.space
|
||||
const templates = space && space.role !== 'diary' ? await api.templates(space.id) : []
|
||||
if (!space || templates.length === 0) return runCommand('new')
|
||||
templateCache = { space: space.id, names: templates.map((t) => t.slug) }
|
||||
hooks.openCommandLine(':new ')
|
||||
}
|
||||
|
||||
/** `:encrypt` and `:decrypt`: a file, or everything under a folder. */
|
||||
const sealTarget = (encrypt: boolean) =>
|
||||
workspace.guard(async () => {
|
||||
@@ -1184,7 +1198,7 @@ export const commands: CommandSpec[] = [
|
||||
help: 'full-text search in this space (empty clears)',
|
||||
run: (arg) => workspace.guard(() => workspace.tree.search(arg, false)),
|
||||
bang: {
|
||||
help: 'full-text search across every space — Ctrl-P',
|
||||
help: 'full-text search across every space — `/` then Tab',
|
||||
run: (arg) => workspace.guard(() => workspace.tree.search(arg, true)),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@ export const bindings = [
|
||||
{ context: 'tree', keys: ['gx'], action: 'inTab', help: 'open the file under the cursor in a tab of its own' },
|
||||
{ context: 'tree', keys: ['Enter', 'Shift-Enter', 'l', 'ArrowRight'], action: 'open', label: 'Enter · l', help: 'open a document, or enter a folder — on an open keystore or sealed file, Enter asks for its password', essential: true },
|
||||
{ context: 'tree', keys: ['h', 'ArrowLeft'], action: 'left', label: 'h', help: 'from the document to the list, from the list up a level', essential: true },
|
||||
{ context: 'tree', keys: ['o'], action: 'new', help: 'new document here, straight into insert mode', essential: true },
|
||||
{ context: 'tree', keys: ['o'], action: 'new', help: 'new document here, straight into insert mode — when the space has templates, the line offers them first', essential: true },
|
||||
{ context: 'tree', keys: ['O'], action: 'mkdir', caps: ['⇧ O'], help: 'new folder here' },
|
||||
{ context: 'tree', keys: ['i', 'a'], action: 'edit', help: 'edit the open document', essential: true },
|
||||
{ context: 'tree', keys: ['R'], action: 'rename', help: 'rename what is under the cursor — document, folder or card — starting from its name' },
|
||||
@@ -114,12 +114,12 @@ export const bindings = [
|
||||
{ context: 'global', keys: ['C'], action: 'calendar', help: 'the diary by month, on the plan screen: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day among the documents' },
|
||||
{ context: 'tree', keys: ['[d'], action: 'previousEntry', label: '[d / ]d', help: 'the diary entry before / after the one open, by date' },
|
||||
{ context: 'tree', keys: [']d'], action: 'nextEntry' },
|
||||
{ context: 'global', keys: ['Ctrl-p'], action: 'find', label: 'Ctrl-P', help: 'find anything, in every space — Enter opens the result lit beside them, n goes on', 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: '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: ['Escape'], action: 'escape' },
|
||||
{ context: 'tree', keys: ['/'], action: 'search', help: 'full-text search, inside the current space and its text files — Tab or 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', 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: '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 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api, ApiError, type Agenda, type AgendaCard, type Board, type BoardList, type Card, type Moved, type NodeSummary, type Task } from '../api'
|
||||
import { api, ApiError, type Agenda, type AgendaCard, type Board, type BoardList, type Card, type Moved, type NodeSummary, type Task, type WorkspaceNode } from '../api'
|
||||
import { formatDay, formatWeekday } from '../util'
|
||||
import type { Workspace } from '../store.svelte'
|
||||
|
||||
@@ -543,19 +543,20 @@ export class Cards {
|
||||
|
||||
/**
|
||||
* Open the document a card points at. A card without one gets one, named
|
||||
* after it, in the space's root — so Enter always leads to a place to write
|
||||
* the notes down. `q` on that document comes back here.
|
||||
* after it, in the folder being browsed when that is in the card's space,
|
||||
* and in the space's root when it is not — so Enter always leads to a
|
||||
* place to write the notes down. `q` on that document comes back here.
|
||||
*/
|
||||
async openCard() {
|
||||
const card = this.card
|
||||
const space = this.boardSpaceId
|
||||
if (!card || space === null) return
|
||||
let nodeId = card.node_id
|
||||
if (nodeId !== null) {
|
||||
let notes: WorkspaceNode | null = null
|
||||
if (card.node_id !== null) {
|
||||
// Deleted notes read as gone, but the card still points at them, so
|
||||
// the way back to them is named rather than a bare "not found".
|
||||
try {
|
||||
await api.getNode(nodeId)
|
||||
notes = await api.getNode(card.node_id)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 404) {
|
||||
return this.ws.say(`the notes of "${card.title}" are gone — :trash may still have them`, 'error')
|
||||
@@ -563,16 +564,17 @@ export class Cards {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
if (nodeId === null) {
|
||||
const node = await api.createNode({ parent_id: space, name: card.title, body: '' })
|
||||
await api.updateCard(card.id, { node_id: node.id })
|
||||
nodeId = node.id
|
||||
if (!notes) {
|
||||
const parent = this.ws.tree.spaceId === space ? (this.ws.tree.parentId ?? space) : space
|
||||
notes = await api.createNode({ parent_id: parent, name: card.title, body: '' })
|
||||
await api.updateCard(card.id, { node_id: notes.id })
|
||||
}
|
||||
if (this.ws.tree.parentId !== space) await this.ws.tree.goTo(space)
|
||||
const folder = notes.parent_id ?? space
|
||||
if (this.ws.tree.parentId !== folder) await this.ws.tree.goTo(folder)
|
||||
else await this.ws.tree.refresh()
|
||||
// The notes open beside the board, and `q` takes their tile away again
|
||||
// on the way back to it.
|
||||
await this.ws.tree.openNode(nodeId, card.node_id === null)
|
||||
await this.ws.tree.openNode(notes.id, card.node_id === null)
|
||||
if (card.node_id === null) this.ws.enterInsert = true
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export class Tree {
|
||||
*/
|
||||
marked = $state.raw<NodeSummary[]>([])
|
||||
|
||||
/** A search across every space rather than the one being browsed — Ctrl-P. */
|
||||
/** A search across every space rather than the one being browsed — `/` then Tab, or `:search!`. */
|
||||
globalSearch = $state(false)
|
||||
|
||||
/** The last thing deleted here, which `u` puts back. */
|
||||
@@ -508,14 +508,20 @@ export class Tree {
|
||||
|
||||
/**
|
||||
* Follow a `[[title]]`: to the document it names, or — when there is none
|
||||
* — to a new one called that, here, after asking. Which document a title
|
||||
* names the server says when the text is written, so it is written first.
|
||||
* — to a new one called that, beside the document linking to it, after
|
||||
* asking. Which document a title names the server says when the text is
|
||||
* written, so it is written first.
|
||||
*/
|
||||
async followTitle(title: string) {
|
||||
await this.ws.drafts.flushTile()
|
||||
const path = this.ws.open?.wikilinks[title]
|
||||
const open = this.ws.open
|
||||
const path = open?.wikilinks[title]
|
||||
if (path) return this.openPath(path)
|
||||
if (!(await this.ws.prompts.ask(`no document called "${title}" yet — write it in ${this.here}?`))) return
|
||||
const crumbs = open?.path.slice(0, -1) ?? []
|
||||
const folder = crumbs.at(-1)
|
||||
const where = folder ? crumbs.map((crumb) => crumb.slug).join('/') : this.here
|
||||
if (!(await this.ws.prompts.ask(`no document called "${title}" yet — write it in ${where}?`))) return
|
||||
if (folder && folder.id !== this.parentId) await this.goTo(folder.id)
|
||||
await this.createDocument(title)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user