Преглед изворни кода

fix(uri): include index in custom qmd links

cocoon пре 1 месец
родитељ
комит
8404cc3bb1
4 измењених фајлова са 78 додато и 7 уклоњено
  1. 26 4
      src/cli/qmd.ts
  2. 8 3
      src/store.ts
  3. 36 0
      test/cli.test.ts
  4. 8 0
      test/store.test.ts

+ 26 - 4
src/cli/qmd.ts

@@ -110,6 +110,7 @@ enableProductionMode();
 
 let store: ReturnType<typeof createStore> | null = null;
 let storeDbPathOverride: string | undefined;
+let currentIndexName = "index";
 
 function getStore(): ReturnType<typeof createStore> {
   if (!store) {
@@ -160,6 +161,10 @@ function getDbPath(): string {
   return store?.dbPath ?? storeDbPathOverride ?? getDefaultDbPath();
 }
 
+function getActiveIndexName(): string {
+  return currentIndexName;
+}
+
 function setIndexName(name: string | null): void {
   let normalizedName = name;
   // Normalize relative paths to prevent malformed database paths
@@ -170,6 +175,7 @@ function setIndexName(name: string | null): void {
     // Replace path separators with underscores to create a valid filename
     normalizedName = absolutePath.replace(/\//g, '_').replace(/^_/, '');
   }
+  currentIndexName = normalizedName || "index";
   storeDbPathOverride = normalizedName ? getDefaultDbPath(normalizedName) : undefined;
   // Reset open handle so next use opens the new index
   closeDb();
@@ -818,8 +824,6 @@ function contextRemove(pathArg: string): void {
 }
 
 function getDocument(filename: string, fromLine?: number, maxLines?: number, lineNumbers?: boolean): void {
-  const db = getDb();
-
   // Parse :linenum suffix from filename (e.g., "file.md:100")
   let inputPath = filename;
   const colonMatch = inputPath.match(/:(\d+)$/);
@@ -831,6 +835,14 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin
     }
   }
 
+  const parsedIndexPath = isVirtualPath(inputPath) ? parseVirtualPath(inputPath) : null;
+  if (parsedIndexPath?.indexName) {
+    setIndexName(parsedIndexPath.indexName);
+    setConfigIndexName(parsedIndexPath.indexName);
+  }
+
+  const db = getDb();
+
   // Handle docid lookup (#abc123, abc123, "#abc123", "abc123", etc.)
   if (isDocid(inputPath)) {
     const docidMatch = findDocumentByDocid(db, inputPath);
@@ -842,7 +854,6 @@ function getDocument(filename: string, fromLine?: number, maxLines?: number, lin
       process.exit(1);
     }
   }
-
   let doc: { collectionName: string; path: string; body: string } | null = null;
   let virtualPath: string;
 
@@ -1925,7 +1936,18 @@ function outputResults(results: OutputRow[], query: string, opts: OutputOptions)
   }
 
   // Helper to create qmd:// URI from displayPath
-  const toQmdPath = (displayPath: string) => `qmd://${displayPath}`;
+  const toQmdPath = (displayPath: string) => {
+    const [collectionName, ...segments] = displayPath.split("/");
+    if (!collectionName || segments.length === 0) {
+      return `qmd://${displayPath}`;
+    }
+    const indexName = getActiveIndexName();
+    return buildVirtualPath(
+      collectionName,
+      segments.join("/"),
+      indexName === "index" ? undefined : indexName,
+    );
+  };
 
   if (opts.format === "json") {
     // JSON output for LLM consumption

+ 8 - 3
src/store.ts

@@ -566,6 +566,7 @@ export function getRealPath(path: string): string {
 export type VirtualPath = {
   collectionName: string;
   path: string;  // relative path within collection
+  indexName?: string;
 };
 
 /**
@@ -609,22 +610,26 @@ export function normalizeVirtualPath(input: string): string {
 export function parseVirtualPath(virtualPath: string): VirtualPath | null {
   // Normalize the path first
   const normalized = normalizeVirtualPath(virtualPath);
+  const [pathPart = normalized, queryString = ""] = normalized.split("?");
 
   // Match: qmd://collection-name[/optional-path]
   // Allows: qmd://name, qmd://name/, qmd://name/path
-  const match = normalized.match(/^qmd:\/\/([^\/]+)\/?(.*)$/);
+  const match = pathPart.match(/^qmd:\/\/([^\/]+)\/?(.*)$/);
   if (!match?.[1]) return null;
+  const indexName = new URLSearchParams(queryString).get("index")?.trim() || undefined;
   return {
     collectionName: match[1],
     path: match[2] ?? '',  // Empty string for collection root
+    ...(indexName ? { indexName } : {}),
   };
 }
 
 /**
  * Build a virtual path from collection name and relative path.
  */
-export function buildVirtualPath(collectionName: string, path: string): string {
-  return `qmd://${collectionName}/${path}`;
+export function buildVirtualPath(collectionName: string, path: string, indexName?: string): string {
+  const base = `qmd://${collectionName}/${path}`;
+  return indexName ? `${base}?index=${encodeURIComponent(indexName)}` : base;
 }
 
 /**

+ 36 - 0
test/cli.test.ts

@@ -1130,6 +1130,42 @@ describe("search output formats", () => {
     expect(result.file).not.toMatch(/^\/home\//);
   });
 
+  test("custom-index search links include ?index= and can be passed back to qmd get", async () => {
+    const env = await createIsolatedTestEnv("custom-index-links");
+    const customColl = "fixtures-alt";
+    const customIndex = "release-notes";
+    const customCacheDir = join(testDir, `cache-${Date.now()}-${Math.random().toString(16).slice(2)}`);
+    await mkdir(customCacheDir, { recursive: true });
+
+    const sharedEnv = {
+      INDEX_PATH: "",
+      XDG_CACHE_HOME: customCacheDir,
+    };
+
+    const addResult = await runQmd(
+      ["--index", customIndex, "collection", "add", fixturesDir, "--name", customColl],
+      { dbPath: env.dbPath, configDir: env.configDir, env: sharedEnv }
+    );
+    expect(addResult.exitCode).toBe(0);
+
+    const searchResult = await runQmd(
+      ["--index", customIndex, "search", "test", "--json", "-n", "1"],
+      { dbPath: env.dbPath, configDir: env.configDir, env: sharedEnv }
+    );
+    expect(searchResult.exitCode).toBe(0);
+
+    const results = JSON.parse(searchResult.stdout);
+    const file = results[0]?.file;
+    expect(file).toMatch(new RegExp(`^qmd://${customColl}/.+\\?index=${customIndex}$`));
+
+    const getResult = await runQmd(
+      ["get", file, "-l", "2"],
+      { dbPath: env.dbPath, configDir: env.configDir, env: sharedEnv }
+    );
+    expect(getResult.exitCode).toBe(0);
+    expect(getResult.stdout.trim().length).toBeGreaterThan(0);
+  });
+
   test("search --files includes qmd:// path, docid, and context", async () => {
     const { stdout, exitCode } = await runQmd(["search", "test", "--files", "-n", "1"], { dbPath: localDbPath, configDir: localConfigDir });
     expect(exitCode).toBe(0);

+ 8 - 0
test/store.test.ts

@@ -3154,6 +3154,14 @@ describe("parseVirtualPath", () => {
     });
   });
 
+  test("parses qmd:// paths with index query parameters", () => {
+    expect(parseVirtualPath("qmd://collection/path.md?index=docs-v2")).toEqual({
+      collectionName: "collection",
+      path: "path.md",
+      indexName: "docs-v2",
+    });
+  });
+
   test("returns null for non-virtual paths", () => {
     expect(parseVirtualPath("/absolute/path.md")).toBe(null);
     expect(parseVirtualPath("~/home/path.md")).toBe(null);