Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ jobs:
- run: npm ci
- run: npm run lint
- run: npm test
- run: npx tsc --noEmit
- run: npm run build
- run: npx tsc --noEmit
48 changes: 48 additions & 0 deletions src/lib/capture-choice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { resolveCaptureChoice } from "./capture-choice";
import type { ChoiceSummary } from "./types";

const choice = (
id: string,
name: string,
type: ChoiceSummary["type"] = "Capture",
): ChoiceSummary => ({
id,
name,
type,
command: false,
path: name,
runnable: type !== "Multi",
});

describe("resolveCaptureChoice", () => {
it("returns the one choice with that name", () => {
expect(
resolveCaptureChoice(
[choice("a", "Inbox"), choice("b", "Journal")],
"Inbox",
).id,
).toBe("a");
});

it("prefers the Capture when a Template shares the name", () => {
const choices = [choice("t", "Inbox", "Template"), choice("c", "Inbox")];
expect(resolveCaptureChoice(choices, "Inbox").id).toBe("c");
});

it("refuses two Captures with the same name instead of guessing", () => {
const choices = [choice("x", "Inbox"), choice("y", "Inbox")];
expect(() => resolveCaptureChoice(choices, "Inbox")).toThrow(/Several/);
});

it("names the missing choice", () => {
expect(() => resolveCaptureChoice([choice("a", "Inbox")], "Nope")).toThrow(
/"Nope"/,
);
});

it("ignores a Multi folder that shares the name", () => {
const choices = [choice("m", "Inbox", "Multi"), choice("c", "Inbox")];
expect(resolveCaptureChoice(choices, "Inbox").id).toBe("c");
});
});
22 changes: 22 additions & 0 deletions src/lib/capture-choice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ChoiceSummary } from "./types";

/**
* The capture preference stores a name, but QuickAdd runs choices by id and
* two choices in different Multi folders can share a name.
*/
export function resolveCaptureChoice(
choices: ChoiceSummary[],
name: string,
): ChoiceSummary {
const wanted = name.trim();
const named = choices.filter((c) => c.runnable && c.name === wanted);
if (named.length === 1) return named[0];
if (named.length === 0) {
throw new Error(`No QuickAdd choice is named "${wanted}".`);
}
const captures = named.filter((c) => c.type === "Capture");
if (captures.length === 1) return captures[0];
throw new Error(
`Several QuickAdd choices are named "${wanted}". Rename one of them.`,
);
}
16 changes: 7 additions & 9 deletions src/lib/capture.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Toast, getPreferenceValues, showToast } from "@raycast/api";
import { showFailureToast } from "@raycast/utils";
import { doneMessage } from "./interactive";
import { prepareVault, runChoiceByName } from "./obsidianCli";
import { listChoices, prepareVault, runChoice } from "./obsidianCli";
import { resolveCaptureChoice } from "./capture-choice";
import { chooseVault, readRegistry } from "./vaults";

interface CapturePreferences {
captureChoice: string;
vaultPath?: string;
}

export interface CaptureContext {
vaultPath?: string;
}
Expand All @@ -18,7 +14,7 @@ export async function capture(
contextVaultPath: string | undefined,
): Promise<void> {
const { captureChoice, vaultPath } =
getPreferenceValues<CapturePreferences>();
getPreferenceValues<Preferences.QuickCapture>();

const toast = await showToast({
style: Toast.Style.Animated,
Expand All @@ -36,14 +32,16 @@ export async function capture(
}
const ready = await prepareVault(chosen.vault, undefined, registry);
if (!ready.ok) throw new Error(ready.message);
const result = await runChoiceByName(chosen.vault, captureChoice, {
const listed = await listChoices(chosen.vault);
const choice = resolveCaptureChoice(listed.choices ?? [], captureChoice);
const result = await runChoice(chosen.vault, choice.id, {
vars: { value: text },
});
if (!result.ok) {
throw new Error(result.error ?? "Capture failed");
}
toast.style = Toast.Style.Success;
toast.title = doneMessage(captureChoice, result);
toast.title = doneMessage(choice.name, result);
} catch (error) {
await toast.hide();
await showFailureToast(error, { title: "Could not capture" });
Expand Down
5 changes: 5 additions & 0 deletions src/lib/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,9 @@ describe("insertTag", () => {
it("replaces the # with the tag and keeps the text after it", () => {
expect(insertTag("a # c", 2, "work")).toBe("a #work c");
});

it("adds a space so the next word does not join the tag", () => {
expect(insertTag("a #", 2, "work")).toBe("a #work ");
expect(insertTag("a #later", 2, "work")).toBe("a #work later");
});
});
4 changes: 3 additions & 1 deletion src/lib/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@ export function insertLink(value: string, at: number, text: string): string {
}

export function insertTag(value: string, at: number, tag: string): string {
return `${value.slice(0, at)}#${tag}${value.slice(at + 1)}`;
const rest = value.slice(at + 1);
const separator = /^\s/.test(rest) ? "" : " ";
return `${value.slice(0, at)}#${tag}${separator}${rest}`;
}
15 changes: 0 additions & 15 deletions src/lib/obsidianCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ const CLI_CANDIDATES = [
`${homedir()}/Applications/Obsidian.app/Contents/MacOS/obsidian-cli`,
];

interface Preferences {
cliPath?: string;
}

/** Raised for transport-level failures (CLI missing, Obsidian/vault unreachable). */
export class ObsidianCliError extends Error {}

Expand Down Expand Up @@ -196,17 +192,6 @@ export async function runChoice(
});
}

export async function runChoiceByName(
vault: Vault,
name: string,
options: RunOptions = {},
): Promise<RunResponse> {
return invoke<RunResponse>(vault, "quickadd:run", {
choice: name,
...runParams(options),
});
}

export function obsidianOpenUrl(
vault: Vault,
vaultRelativePath?: string,
Expand Down
4 changes: 0 additions & 4 deletions src/run-choice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ export default function RunChoiceCommand(
return <VaultGate vault={chosen.vault} registry={registry} {...context} />;
}

interface Preferences {
vaultPath?: string;
}

function runChoiceDeeplink(context: RunChoiceContext): string {
return createDeeplink({ command: "run-choice", context });
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["src/**/*"],
"include": ["src/**/*", "raycast-env.d.ts"],
"compilerOptions": {
"lib": ["ES2023"],
"module": "commonjs",
Expand Down
Loading