Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 11 additions & 1 deletion extensions/numpy-documentation-search/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# NumPy Documentation Search Changelog

## [1.2.3] - 2025-10-09
## [1.3.0] - {PR_MERGE_DATE}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changelog now includes {PR_MERGE_DATE} placeholders (and replaces the previously concrete date for 1.2.3). If the repository expects released versions to have real dates, these placeholders should be replaced with actual dates before merge/release so the changelog remains accurate and user-facing.

Copilot uses AI. Check for mistakes.

### Added
- Add guided recovery when live access to `numpy.org` fails, including in-app setup steps for downloaded local docs
- Add configurable documentation source preferences for auto and local directory modes

### Changed
- Fall back to local documentation when remote inventory loading fails
- Load inline documentation previews from a configured local docs directory when network access is unavailable

## [1.2.3] - {PR_MERGE_DATE}
Comment thread
0xdhrv marked this conversation as resolved.
Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Previously-released entry date overwritten with placeholder

The [1.2.3] entry already had a real merge date (2025-10-09) from a prior PR. Changing it to {PR_MERGE_DATE} will cause the release tooling to stamp it with this PR's merge date, permanently erasing the historical date.

Suggested change
## [1.2.3] - {PR_MERGE_DATE}
## [1.2.3] - 2025-10-09
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/numpy-documentation-search/CHANGELOG.md
Line: 13

Comment:
**Previously-released entry date overwritten with placeholder**

The `[1.2.3]` entry already had a real merge date (`2025-10-09`) from a prior PR. Changing it to `{PR_MERGE_DATE}` will cause the release tooling to stamp it with this PR's merge date, permanently erasing the historical date.

```suggestion
## [1.2.3] - 2025-10-09
```

How can I resolve this? If you propose a fix, please make it concise.


### Changed
- Refresh README to focus on a concise Raycast user overview with key in-app features
Expand Down
14 changes: 14 additions & 0 deletions extensions/numpy-documentation-search/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,17 @@ A Raycast extension that lets you search the NumPy API reference and preview det
- **Rich previews**: See inline call signatures, parameter tables, return values, and version notes without leaving Raycast.
- **Doc deep links**: Open the exact section on numpy.org in your browser when you need the full documentation context.
- **Copy-ready snippets**: Copy function signatures or doc URLs directly from the command for fast handoff into notebooks, scripts, or PRs.

## Certificate-Restricted Networks

If your work network blocks access to `https://numpy.org/doc/stable/`, the extension now shows a guided recovery state instead of only surfacing the fetch error.

You can point the command at downloaded local documentation:

1. Obtain a generated NumPy docs copy from [numpy/doc](https://github.com/numpy/doc) or from another machine that can reach `numpy.org`.
2. Extract the download locally and select the downloaded docs folder in Raycast. The extension will load the documentation from its `stable` subfolder.
3. Open the command preferences and set:
- `Documentation Source` to `Local Docs Directory`, or leave it on `Online`
- `Local Docs Directory` to the downloaded docs folder

If no local docs are configured, the extension requires live access to `numpy.org`. Local HTML docs provide full inline previews without network access.
337 changes: 146 additions & 191 deletions extensions/numpy-documentation-search/package-lock.json

Large diffs are not rendered by default.

29 changes: 27 additions & 2 deletions extensions/numpy-documentation-search/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "numpy-documentation-search",
"title": "NumPy Documentation Search",
"title": "Numpy Documentation Search",
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extension/command titles changed from NumPy to Numpy. NumPy is a proper name with specific capitalization and this change will surface in the Raycast Store and command UI. Recommend reverting these strings to NumPy for correctness and consistency with the rest of the extension text (numpy.org, README, etc.).

Copilot uses AI. Check for mistakes.
"description": "Quickly search through official NumPy documentation",
"icon": "extension-icon.png",
"author": "FariaF22",
Expand All @@ -18,13 +18,38 @@
"commands": [
{
"name": "numpy-docs",
"title": "NumPy Docs",
"title": "Numpy Docs",
"subtitle": "Search through Numpy documentation",
Comment on lines +21 to 22
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extension/command titles changed from NumPy to Numpy. NumPy is a proper name with specific capitalization and this change will surface in the Raycast Store and command UI. Recommend reverting these strings to NumPy for correctness and consistency with the rest of the extension text (numpy.org, README, etc.).

Copilot uses AI. Check for mistakes.
"description": "Search NumPy's API reference and preview documentation inside Raycast.",
"mode": "view"
}
],
"preferences": [
{
"name": "documentationSourceMode",
"type": "dropdown",
"required": true,
"title": "Documentation Source",
"description": "Choose where the extension should load NumPy documentation from",
"default": "online",
"data": [
{
"title": "Online",
"value": "online"
},
{
"title": "Local Docs Directory",
"value": "local"
}
]
},
{
"name": "localDocsDirectory",
"type": "directory",
"required": false,
"title": "Local Docs Directory",
"description": "Only used when Documentation Source is Local Docs Directory. Select the downloaded NumPy docs folder and the extension will load files from its stable subfolder"
},
{
"name": "useShortPrefix",
"type": "checkbox",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { deflateSync } from "node:zlib";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadDocDetail, loadInventory } from "../lib/docs-source";
import type { InventoryItem } from "../lib/inventory";

const linspaceItem: InventoryItem = {
id: "numpy.linspace",
name: "numpy.linspace",
shortName: "linspace",
role: "py:function",
url: "https://numpy.org/doc/stable/reference/generated/numpy.linspace.html",
docPath: "reference/generated/numpy.linspace.html#numpy.linspace",
displayName: "numpy.linspace",
};

const tempDirs: string[] = [];

afterEach(() => {
for (const dir of tempDirs) {
rmSync(dir, { force: true, recursive: true });
}
tempDirs.length = 0;
});

describe("loadInventory", () => {
it("falls back to local docs in online mode when remote loading fails", async () => {
const localDocsDirectory = createLocalDocsDirectory();
const stableDir = path.join(localDocsDirectory, "stable");
mkdirSync(stableDir, { recursive: true });
writeFileSync(path.join(stableDir, "objects.inv"), buildInventoryFixture());

const result = await loadInventory(
{
localDocsDirectory,
mode: "online",
},
{
fetchImpl: vi.fn(async () => {
throw new Error("certificate failure");
}) as typeof fetch,
readBinaryFileImpl: async (filePath) => readFileSync(filePath),
readTextFileImpl: async (filePath) => readFileSync(filePath, "utf8"),
},
);

expect(result.source).toBe("local");
expect(result.remoteError?.message).toBe("certificate failure");
expect(result.data.some((item) => item.id === "numpy.linspace")).toBe(true);
});

it("surfaces the remote error when online mode has no local docs fallback", async () => {
await expect(
loadInventory(
{
mode: "online",
},
{
fetchImpl: vi.fn(async () => {
throw new Error("certificate failure");
}) as typeof fetch,
readBinaryFileImpl: async (filePath) => readFileSync(filePath),
readTextFileImpl: async (filePath) => readFileSync(filePath, "utf8"),
},
),
).rejects.toThrow("certificate failure");
});
});

describe("loadDocDetail", () => {
it("loads documentation detail from a local docs directory", async () => {
const localDocsDirectory = createLocalDocsDirectory();
const referenceDir = path.join(localDocsDirectory, "stable/reference/generated");
mkdirSync(referenceDir, { recursive: true });
writeFileSync(
path.join(referenceDir, "numpy.linspace.html"),
readFileSync(path.join(process.cwd(), "src/__tests__/fixtures/numpy.linspace.html"), "utf8"),
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using process.cwd() makes the test dependent on the runner’s working directory (often the monorepo root), which can cause failures when executed from different paths/CI setups. Prefer resolving the fixture relative to the test file location (e.g., via import.meta.url / __dirname equivalent) to make the test location-independent.

Suggested change
readFileSync(path.join(process.cwd(), "src/__tests__/fixtures/numpy.linspace.html"), "utf8"),
readFileSync(new URL("./fixtures/numpy.linspace.html", import.meta.url), "utf8"),

Copilot uses AI. Check for mistakes.
);

const result = await loadDocDetail({
inventorySource: "local",
item: linspaceItem,
localDocsDirectory,
mode: "local",
});

expect(result.source).toBe("local");
expect(result.data?.signature).toContain("numpy.linspace");
});
});

function createLocalDocsDirectory(): string {
const dir = mkdtempSync(path.join(tmpdir(), "numpy-docs-"));
tempDirs.push(dir);
return dir;
}

function buildInventoryFixture(): Buffer {
const header = [
"# Sphinx inventory version 2",
"# Project: NumPy",
"# Version: stable",
"# The remainder of this file is compressed using zlib.",
].join("\n");
const body = "numpy.linspace py:function 1 reference/generated/numpy.linspace.html#numpy.linspace -";

return Buffer.concat([Buffer.from(`${header}\n`, "utf8"), deflateSync(`${body}\n`)]);
}
97 changes: 82 additions & 15 deletions extensions/numpy-documentation-search/src/hooks/useDocDetail.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,94 @@
import { useFetch } from "@raycast/utils";
import { useCallback, useEffect, useState } from "react";
import { loadDocDetail, type DocumentationSourceMode, type ResolvedDocumentationSource } from "../lib/docs-source";
import type { DocDetail } from "../lib/doc-detail";
import type { InventoryItem } from "../lib/inventory";
import { parseDocDetail, type DocDetail } from "../lib/doc-detail";

interface UseDocDetailResult {
data?: DocDetail;
isLoading: boolean;
error?: Error;
revalidate: () => void;
source?: ResolvedDocumentationSource;
}

export function useDocDetail(item: InventoryItem | undefined): UseDocDetailResult {
const { data, isLoading, error, revalidate } = useFetch<DocDetail>(item?.url ?? "", {
execute: Boolean(item),
keepPreviousData: true,
parseResponse: async (response) => {
if (!response.ok) {
throw new Error(`Failed to load documentation: ${response.status} ${response.statusText}`);
}

const html = await response.text();
return parseDocDetail(html, item!);
},
interface UseDocDetailOptions {
inventorySource?: ResolvedDocumentationSource;
item: InventoryItem | undefined;
localDocsDirectory?: string;
mode: DocumentationSourceMode;
}

export function useDocDetail(options: UseDocDetailOptions): UseDocDetailResult {
const [reloadToken, setReloadToken] = useState(0);

const revalidate = useCallback(() => {
setReloadToken((token) => token + 1);
}, []);

const [state, setState] = useState<UseDocDetailResult>({
isLoading: false,
revalidate,
});

return { data, isLoading, error, revalidate };
useEffect(() => {
let cancelled = false;

if (!options.item || !options.inventorySource) {
setState({
data: undefined,
error: undefined,
isLoading: false,
revalidate,
source: undefined,
});
return () => {
cancelled = true;
};
}

setState((current) => ({
...current,
error: undefined,
isLoading: true,
revalidate,
}));

void loadDocDetail({
inventorySource: options.inventorySource,
item: options.item,
localDocsDirectory: options.localDocsDirectory,
mode: options.mode,
})
.then((result) => {
if (cancelled) {
return;
}

setState({
data: result.data,
error: undefined,
isLoading: false,
revalidate,
source: result.source,
});
})
.catch((error: unknown) => {
if (cancelled) {
return;
}

setState((current) => ({
...current,
error: error instanceof Error ? error : new Error(String(error)),
isLoading: false,
revalidate,
}));
});

return () => {
cancelled = true;
};
}, [options.inventorySource, options.item, options.localDocsDirectory, options.mode, reloadToken, revalidate]);

return state;
}
Loading
Loading