diff --git a/docs/src/content/docs/docs/Choices/CaptureChoice.md b/docs/src/content/docs/docs/Choices/CaptureChoice.md index 5e56c5b6..b5676595 100644 --- a/docs/src/content/docs/docs/Choices/CaptureChoice.md +++ b/docs/src/content/docs/docs/Choices/CaptureChoice.md @@ -92,6 +92,11 @@ an existing note in scope, so typing an existing name selects it instead of offering a duplicate. The picker still opens for an empty folder, tag, property, or filtered scope so you can create the first note there. +:::note[Available in the next release] +The create row is selected first, so press Enter to create the exact name you +typed even when existing notes are fuzzy matches. +::: + ### Capture to a folder {#capturing-to-folders} Type a folder name (like `CRM/people`) and QuickAdd asks which note in that diff --git a/src/gui/InputSuggester/inputSuggester.test.ts b/src/gui/InputSuggester/inputSuggester.test.ts index 4db82171..fcc508b5 100644 --- a/src/gui/InputSuggester/inputSuggester.test.ts +++ b/src/gui/InputSuggester/inputSuggester.test.ts @@ -24,6 +24,27 @@ describe("InputSuggester", () => { expect(suggestions[suggestions.length - 1]?.item).toBe("bet"); }); + it("places labeled create suggestions before fuzzy matches", () => { + const suggester = new InputSuggester( + app, + ["New note 01", "New note 02"], + ["Folder/New note 01.md", "Folder/New note 02.md"], + { + customValueLabel: (value) => `Create new note: ${value}`, + }, + ); + + suggester.inputEl.value = "New note"; + + const suggestions = suggester.getSuggestions("New note"); + + expect(suggestions.map((suggestion) => suggestion.item)).toEqual([ + "New note", + "Folder/New note 01.md", + "Folder/New note 02.md", + ]); + }); + it("avoids duplicating existing items as custom input", () => { const suggester = new InputSuggester( app, diff --git a/src/gui/InputSuggester/inputSuggester.ts b/src/gui/InputSuggester/inputSuggester.ts index f9e9f3df..1d2938ef 100644 --- a/src/gui/InputSuggester/inputSuggester.ts +++ b/src/gui/InputSuggester/inputSuggester.ts @@ -29,7 +29,8 @@ type Options = { allowCustomValue: boolean; /** * Renders the typed-but-unmatched custom row with a label, e.g. - * `(value) => \`Create new note: ${value}\``. Implies a "create" affordance. + * `(value) => \`Create new note: ${value}\``. Implies a "create" affordance + * that is placed first so Enter performs the labeled action. */ customValueLabel: (value: string) => string; /** @@ -198,13 +199,19 @@ export default class InputSuggester extends FuzzySuggestModal { return suggestions; } - suggestions.push({ + const customSuggestion = { item: customValue, match: { score: Number.NEGATIVE_INFINITY, matches: [], }, - }); + }; + + if (this.customValueLabel) { + suggestions.unshift(customSuggestion); + } else { + suggestions.push(customSuggestion); + } return suggestions; } diff --git a/tests/orb-setup.test.ts b/tests/orb-setup.test.ts index 2f013841..d5d70849 100644 --- a/tests/orb-setup.test.ts +++ b/tests/orb-setup.test.ts @@ -9,7 +9,10 @@ const repoRoot = path.resolve(import.meta.dirname, ".."); const runE2E = path.join(repoRoot, ".agents", "run-e2e"); const openShim = path.join(repoRoot, ".agents", "obsidian-open"); const patcher = path.join(repoRoot, ".agents", "patch-obsidian-e2e-linux.mjs"); -const profileRoot = "/tmp/quickadd-obsidian-e2e"; +const profileRoot = path.join( + fs.realpathSync("/tmp"), + "quickadd-obsidian-e2e", +); const temporaryPaths: string[] = []; interface ProfileRootState { @@ -173,7 +176,9 @@ exit 0 }); }); -describe("Linux Obsidian launcher validation", () => { +const describeLinux = process.platform === "linux" ? describe : describe.skip; + +describeLinux("Linux Obsidian launcher validation", () => { function validArgs(validHome: string): string[] { return [ "-n",