From d825b149fd5c15c9365dcc023bfad3a444a0187a Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Fri, 23 Oct 2026 12:45:50 -0500 Subject: [PATCH 01/10] Fix(search): note search, the buffer switcher and the [[ picker name a folder the same way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2.75.0 gave note search a folder column (#982), and it showed the whole vault path: `inbox/Projects/Atlas/docs`. The [[ picker already showed the same note as `Projects/Atlas/docs/`, and the buffer switcher showed the raw parent directory, so one note read three ways depending on where you looked. The picker had the opposite problem one area over: it stripped every area's directory, so an archived `2025/Atlas/` looked like an inbox folder of the same name. All three now ask one helper, `noteFolderLabel` in vault-layout. In the main notes area it is the folder inside the area, without the area's own directory, following a root-mode vault and a remapped inbox the way the sidebar does. Anywhere else it is the folder from the vault root, area first (`archive/2025/Atlas`, or the remapped directory name), so an archived note never passes for an inbox one. A note at the top of the main notes area has no folder and gets '': each surface keeps its own area label there (note search's INBOX column, the picker's `inbox/`, the switcher's `inbox`), since that label is surface styling, not a path. A buffer whose note the store does not list still falls back to its path's parent. The bidi embedding around the search folder is now written as ‪ and ‬ escapes, like the buffer switcher's, instead of invisible literals. --- .../src/components/BufferPalette.test.ts | 26 +++++++++++ .../app-core/src/components/BufferPalette.tsx | 27 ++++++++--- .../src/components/SearchPalette.test.ts | 10 ++--- .../app-core/src/components/SearchPalette.tsx | 20 ++++----- .../app-core/src/lib/link-candidates.test.ts | 22 ++++++++- packages/app-core/src/lib/link-candidates.ts | 14 +++--- .../app-core/src/lib/vault-layout.test.ts | 45 +++++++++++++++++++ packages/app-core/src/lib/vault-layout.ts | 18 ++++++++ 8 files changed, 153 insertions(+), 29 deletions(-) diff --git a/packages/app-core/src/components/BufferPalette.test.ts b/packages/app-core/src/components/BufferPalette.test.ts index 999bf4426..504ba84f6 100644 --- a/packages/app-core/src/components/BufferPalette.test.ts +++ b/packages/app-core/src/components/BufferPalette.test.ts @@ -3,6 +3,7 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { DEFAULT_VAULT_SETTINGS, type NoteFolder, type NoteMeta } from '@shared/ipc' import { findLeaf, type PaneLeaf } from '../lib/pane-layout' import { useStore } from '../store' import { BufferPalette } from './BufferPalette' @@ -109,4 +110,29 @@ describe('BufferPalette (#641)', () => { 'B' ) }) + + it('shows the folder note search and the [[ picker show for each buffer', () => { + const note = (path: string, folder: NoteFolder): NoteMeta => + ({ path, folder, title: 'Overview', tags: [], wikilinks: [] }) as unknown as NoteMeta + const tabs = [ + 'inbox/Projects/Atlas/Overview.md', + 'archive/2025/Atlas/Overview.md', + 'inbox/Overview.md', + 'inbox/Unlisted/Overview.md' + ] + useStore.setState({ + paneLayout: { kind: 'leaf', id: 'pane-a', tabs, pinnedTabs: [], activeTab: tabs[0] }, + selectedPath: tabs[0], + notes: [note(tabs[0], 'inbox'), note(tabs[1], 'archive'), note(tabs[2], 'inbox')], + vaultSettings: { ...DEFAULT_VAULT_SETTINGS, primaryNotesLocation: 'inbox' } + }) + act(() => root.render(createElement(BufferPalette))) + // Natural order: the hidden tabs, then the current one. A tab whose note + // is not listed falls back to its path's folder. + expect( + [...document.querySelectorAll('[data-buf-idx] span[dir="rtl"]')].map((span) => + span.textContent?.replace(/[\u202A\u202C]/g, '') + ) + ).toEqual(['archive/2025/Atlas', 'inbox', 'inbox/Unlisted', 'Projects/Atlas']) + }) }) diff --git a/packages/app-core/src/components/BufferPalette.tsx b/packages/app-core/src/components/BufferPalette.tsx index 8a4ac349a..6637a2987 100644 --- a/packages/app-core/src/components/BufferPalette.tsx +++ b/packages/app-core/src/components/BufferPalette.tsx @@ -29,6 +29,7 @@ import { isTrashTabPath } from '@shared/trash' import { isQuickNotesTabPath } from '@shared/quick-notes' import { resolveSystemFolderLabels, type SystemFolderLabels } from '../lib/system-folder-labels' import { focusEditorNormalMode } from '../lib/editor-focus' +import { noteFolderLabel } from '../lib/vault-layout' import { Modal } from './ui/Modal' interface BufferEntry { @@ -47,9 +48,11 @@ function fallbackTitle(path: string): string { return filename.replace(/\.md$/i, '') || path } -/** The row's secondary column: the note's folder, not the full path — the - * filename part would just repeat the title, and it was crowding the title - * out of the row (#635). The full path stays searchable via keywords. */ +/** The row's secondary column: the note's folder, not the full path, since + * the filename part would just repeat the title, and it was crowding the title + * out of the row (#635). The full path stays searchable via keywords. A note + * the store knows gets the folder note search and the [[ picker show + * (`noteFolderLabel`); this is only for a tab whose note is not listed. */ function parentDir(path: string): string { const idx = path.lastIndexOf('/') return idx > 0 ? path.slice(0, idx) : '' @@ -63,6 +66,7 @@ interface BuildDeps { notes: ReturnType['notes'] noteDirty: ReturnType['noteDirty'] systemFolderLabels: SystemFolderLabels + vaultSettings: ReturnType['vaultSettings'] } function buildEntries(deps: BuildDeps): BufferEntry[] { @@ -201,7 +205,7 @@ function buildEntries(deps: BuildDeps): BufferEntry[] { entries.push({ path, title, - subtitle: parentDir(path), + subtitle: meta ? noteFolderLabel(meta, deps.vaultSettings) || meta.folder : parentDir(path), keywords: [title, path, meta?.folder].filter(Boolean).join(' '), badge, current: isCurrent, @@ -249,6 +253,7 @@ export function BufferPalette(): JSX.Element { const notes = useStore((s) => s.notes) const noteDirty = useStore((s) => s.noteDirty) const systemFolderLabels = useStore((s) => s.systemFolderLabels) + const vaultSettings = useStore((s) => s.vaultSettings) const entries = useMemo( () => @@ -259,9 +264,19 @@ export function BufferPalette(): JSX.Element { noteContents, notes, noteDirty, - systemFolderLabels + systemFolderLabels, + vaultSettings }), - [paneLayout, activePaneId, selectedPath, noteContents, notes, noteDirty, systemFolderLabels] + [ + paneLayout, + activePaneId, + selectedPath, + noteContents, + notes, + noteDirty, + systemFolderLabels, + vaultSettings + ] ) const [query, setQuery] = useState('') diff --git a/packages/app-core/src/components/SearchPalette.test.ts b/packages/app-core/src/components/SearchPalette.test.ts index f8789b9a1..f3772ffb9 100644 --- a/packages/app-core/src/components/SearchPalette.test.ts +++ b/packages/app-core/src/components/SearchPalette.test.ts @@ -295,7 +295,7 @@ describe('SearchPalette: the create row opens a New note form', () => { type(search(), 'Alph') const rows = [...document.querySelectorAll('[data-search-idx]')] expect(rows.map((r) => r.dataset.searchIdx)).toEqual(['0', '1']) - expect(rows[0].textContent).toBe('Alpha‪inbox‬') + expect(rows[0].textContent).toBe('Alphainbox') expect(rows[1].hasAttribute('data-search-create')).toBe(true) expect(highlighted()).toBe('0') @@ -609,14 +609,14 @@ describe('SearchPalette: each row shows the note\'s folder (#982)', () => { const shown = (): { title: string; folder: string }[] => [...document.querySelectorAll('[data-search-idx]')].map((row) => { const [title, folder] = [...row.querySelectorAll('span')].map((s) => s.textContent ?? '') - return { title, folder: folder.replace(/[‪‬]/g, '') } + return { title, folder: folder.replace(/[\u202A\u202C]/g, '') } }) it('tells notes that share a name apart by their folder', () => { act(() => root.render(createElement(SearchPalette))) expect(shown()).toEqual([ - { title: 'Overview', folder: 'inbox/Projects/Atlas/docs' }, - { title: 'Overview', folder: 'inbox/Projects/Billing/docs' }, + { title: 'Overview', folder: 'Projects/Atlas/docs' }, + { title: 'Overview', folder: 'Projects/Billing/docs' }, { title: 'Overview', folder: 'archive/2025/Atlas' }, { title: 'Overview', folder: 'inbox' } ]) @@ -629,7 +629,7 @@ describe('SearchPalette: each row shows the note\'s folder (#982)', () => { for (const dir of dirs) { // rtl puts the ellipsis on the left; the embedding keeps the path LTR. expect(dir.getAttribute('dir')).toBe('rtl') - expect(dir.textContent?.startsWith('‪')).toBe(true) + expect(dir.textContent?.startsWith('\u202A')).toBe(true) expect(dir.className).toContain('truncate') } const rootRow = document.querySelector('[data-search-idx="3"]')! diff --git a/packages/app-core/src/components/SearchPalette.tsx b/packages/app-core/src/components/SearchPalette.tsx index 1562f5dae..8c5a30dd9 100644 --- a/packages/app-core/src/components/SearchPalette.tsx +++ b/packages/app-core/src/components/SearchPalette.tsx @@ -9,7 +9,6 @@ import { searchNoteIndex } from '../lib/note-search' import { focusEditorNormalMode } from '../lib/editor-focus' -import { parentDirOf } from '../lib/manual-order' import { destinationLabel, parseDestinationText, @@ -18,7 +17,7 @@ import { type SearchCreateDraft } from '../lib/search-create' import { resolveSystemFolderLabels } from '../lib/system-folder-labels' -import { isPrimaryNotesAtRoot } from '../lib/vault-layout' +import { isPrimaryNotesAtRoot, noteFolderLabel } from '../lib/vault-layout' import { useToastStore } from '../lib/toast' import { Modal } from './ui/Modal' import { SearchCreateForm, type SearchCreateTarget } from './SearchCreateForm' @@ -200,7 +199,7 @@ export function SearchPalette(): JSX.Element {
No matches.
) : ( results.map((n, i) => { - const dir = parentDirOf(n.path) + const dir = noteFolderLabel(n, vaultSettings) return (