Merge branch 'main' into keyword-nav-plan
# Conflicts: # packages/app/src/components/agent-input-area.tsx # packages/app/src/components/command-center.tsx # packages/app/src/hooks/use-global-keyboard-nav.ts
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseInlinePathToken } from "./inline-path";
|
||||
|
||||
describe("parseInlinePathToken", () => {
|
||||
it("returns null for plain paths (no line)", () => {
|
||||
expect(parseInlinePathToken("src/app.ts")).toBeNull();
|
||||
expect(parseInlinePathToken("README.md")).toBeNull();
|
||||
});
|
||||
|
||||
it("parses filename:line", () => {
|
||||
expect(parseInlinePathToken("src/app.ts:12")).toEqual({
|
||||
raw: "src/app.ts:12",
|
||||
path: "src/app.ts",
|
||||
lineStart: 12,
|
||||
lineEnd: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses filename:lineStart-lineEnd", () => {
|
||||
expect(parseInlinePathToken("src/app.ts:12-20")).toEqual({
|
||||
raw: "src/app.ts:12-20",
|
||||
path: "src/app.ts",
|
||||
lineStart: 12,
|
||||
lineEnd: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects range-only :line tokens", () => {
|
||||
expect(parseInlinePathToken(":12")).toBeNull();
|
||||
expect(parseInlinePathToken(":12-20")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
export interface InlinePathTarget {
|
||||
raw: string;
|
||||
path: string;
|
||||
lineStart?: number;
|
||||
lineEnd?: number;
|
||||
}
|
||||
|
||||
function normalizePathToken(value: string): string | null {
|
||||
const trimmed = value
|
||||
.trim()
|
||||
.replace(/^['"`]/, "")
|
||||
.replace(/['"`]$/, "");
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict VSCode-style markers only.
|
||||
*
|
||||
* Supported:
|
||||
* - `filename:linenumber`
|
||||
* - `filename:lineStart-lineEnd`
|
||||
*
|
||||
* Not supported (by design):
|
||||
* - plain `filename` (no line)
|
||||
* - `:linenumber` (range-only)
|
||||
*/
|
||||
export function parseInlinePathToken(value: string): InlinePathTarget | null {
|
||||
const rawValue = value ?? "";
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = trimmed.match(/^(.+?):([0-9]+)(?:-([0-9]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const basePathRaw = match[1]?.trim();
|
||||
if (!basePathRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid accidentally treating URLs as file paths.
|
||||
if (basePathRaw.includes("://")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePathToken(basePathRaw);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineStart = parseInt(match[2], 10);
|
||||
if (!Number.isFinite(lineStart) || lineStart <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineEnd = match[3] ? parseInt(match[3], 10) : undefined;
|
||||
if (lineEnd !== undefined) {
|
||||
if (!Number.isFinite(lineEnd) || lineEnd <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (lineEnd < lineStart) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
raw: rawValue,
|
||||
path: normalizedPath,
|
||||
lineStart,
|
||||
lineEnd,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user